Skip to main content

laser_pdf/elements/
repeat_bottom.rs

1use crate::*;
2
3use self::utils::{add_optional_size, max_optional_size};
4
5pub struct RepeatBottom<C: Element, B: Element> {
6    pub content: C,
7    pub bottom: B,
8    pub gap: f32,
9    pub collapse: bool,
10}
11
12struct Common {
13    first_height: f32,
14    full_height: Option<f32>,
15    bottom_size: ElementSize,
16    bottom_height: f32,
17    pre_break: bool,
18    content_first_location_usage: Option<FirstLocationUsage>,
19}
20
21impl<C: Element, B: Element> RepeatBottom<C, B> {
22    fn common(
23        &self,
24        text_pieces_cache: &TextPiecesCache,
25        width: WidthConstraint,
26        first_height: f32,
27        full_height: Option<f32>,
28    ) -> Common {
29        let bottom_first_height = full_height.unwrap_or(first_height);
30
31        let bottom_size = self.bottom.measure(MeasureCtx {
32            text_pieces_cache,
33            width,
34            first_height: bottom_first_height,
35            breakable: None,
36        });
37
38        let bottom_height = bottom_size.height.map(|h| h + self.gap).unwrap_or(0.);
39
40        let mut first_height = first_height - bottom_height;
41
42        let full_height = full_height.map(|f| f - bottom_height);
43
44        let mut content_first_location_usage = None;
45
46        let pre_break = full_height.is_some_and(|full_height| {
47            first_height < full_height
48                && !self.collapse
49                && (bottom_size.height > Some(first_height)
50                    || *content_first_location_usage.insert(self.content.first_location_usage(
51                        FirstLocationUsageCtx {
52                            text_pieces_cache,
53                            width,
54                            first_height,
55                            full_height,
56                        },
57                    )) == FirstLocationUsage::WillSkip)
58        });
59
60        if pre_break {
61            first_height = full_height.unwrap();
62        }
63
64        Common {
65            bottom_size,
66            bottom_height,
67            first_height,
68            full_height,
69            pre_break,
70            content_first_location_usage,
71        }
72    }
73
74    fn height(&self, common: &Common, height: Option<f32>) -> Option<f32> {
75        height
76            .map(|h| h + self.gap)
77            .or((!self.collapse).then_some(0.))
78            .and_then(|h| add_optional_size(Some(h), common.bottom_size.height))
79    }
80
81    fn size(&self, common: &Common, content_size: ElementSize) -> ElementSize {
82        ElementSize {
83            width: max_optional_size(content_size.width, common.bottom_size.width),
84            height: self.height(common, content_size.height),
85        }
86    }
87}
88
89impl<C: Element, B: Element> Element for RepeatBottom<C, B> {
90    fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
91        let common = self.common(
92            ctx.text_pieces_cache,
93            ctx.width,
94            ctx.first_height,
95            Some(ctx.full_height),
96        );
97
98        if common.pre_break {
99            return FirstLocationUsage::WillSkip;
100        }
101
102        let first_location_usage = common.content_first_location_usage.unwrap_or_else(|| {
103            self.content.first_location_usage(FirstLocationUsageCtx {
104                text_pieces_cache: ctx.text_pieces_cache,
105                width: ctx.width,
106                first_height: common.first_height,
107                full_height: common.full_height.unwrap(),
108            })
109        });
110
111        if first_location_usage == FirstLocationUsage::NoneHeight && !self.collapse {
112            if common.bottom_size.height.is_none() {
113                FirstLocationUsage::NoneHeight
114            } else {
115                FirstLocationUsage::WillUse
116            }
117        } else {
118            first_location_usage
119        }
120    }
121
122    fn measure(&self, mut ctx: MeasureCtx) -> ElementSize {
123        let common = self.common(
124            ctx.text_pieces_cache,
125            ctx.width,
126            ctx.first_height,
127            ctx.breakable.as_ref().map(|b| b.full_height),
128        );
129
130        let mut break_count = 0;
131        let mut extra_location_min_height = None;
132
133        let size = self.content.measure(MeasureCtx {
134            text_pieces_cache: ctx.text_pieces_cache,
135            width: ctx.width,
136            first_height: common.first_height,
137            breakable: ctx.breakable.as_mut().map(|_| BreakableMeasure {
138                full_height: common.full_height.unwrap(),
139                break_count: &mut break_count,
140                extra_location_min_height: &mut extra_location_min_height,
141            }),
142        });
143
144        if let Some(breakable) = ctx.breakable {
145            *breakable.break_count = break_count + u32::from(common.pre_break);
146            *breakable.extra_location_min_height =
147                extra_location_min_height.map(|x| x + common.bottom_height);
148        }
149
150        self.size(&common, size)
151    }
152
153    fn draw(&self, ctx: DrawCtx) -> ElementSize {
154        let common = self.common(
155            ctx.text_pieces_cache,
156            ctx.width,
157            ctx.first_height,
158            ctx.breakable.as_ref().map(|b| b.full_height),
159        );
160
161        let mut current_location = ctx.location.clone();
162
163        let size = if let Some(breakable) = ctx.breakable {
164            let mut break_count = 0;
165
166            let (location, location_offset) = if common.pre_break {
167                current_location = (breakable.do_break)(ctx.pdf, 0, None);
168                (current_location.clone(), 1)
169            } else {
170                (ctx.location, 0)
171            };
172
173            self.content.draw(DrawCtx {
174                pdf: ctx.pdf,
175                text_pieces_cache: ctx.text_pieces_cache,
176                location,
177                width: ctx.width,
178                first_height: common.first_height,
179                preferred_height: ctx.preferred_height.map(|p| p - common.bottom_height),
180                breakable: Some(BreakableDraw {
181                    full_height: common.full_height.unwrap(),
182                    preferred_height_break_count: breakable.preferred_height_break_count,
183                    do_break: &mut |pdf, location_idx, height| {
184                        let location = if location_idx >= break_count {
185                            if let Some(bottom_height) = common
186                                .bottom_size
187                                .height
188                                .filter(|_| height.is_some() || !self.collapse)
189                            {
190                                let first_location_idx = if self.collapse {
191                                    location_idx
192                                } else {
193                                    break_count
194                                };
195
196                                // here i is the location we want to draw on, not the location we break
197                                // break from
198                                for i in first_location_idx..=location_idx {
199                                    let bottom_location = if i == break_count {
200                                        current_location.clone()
201                                    } else {
202                                        (breakable.do_break)(
203                                            pdf,
204                                            location_offset + i - 1,
205                                            // this works because skipped locations have an implied
206                                            // height of None
207                                            (!self.collapse).then_some(bottom_height),
208                                        )
209                                    };
210
211                                    let y_offset = if i == location_idx {
212                                        assert!(height.is_some() || !self.collapse);
213                                        height.map(|h| h + self.gap).unwrap_or(0.)
214                                    } else {
215                                        assert!(!self.collapse);
216                                        0.
217                                    };
218
219                                    let bottom_location = Location {
220                                        pos: (
221                                            bottom_location.pos.0,
222                                            bottom_location.pos.1 - y_offset,
223                                        ),
224                                        ..bottom_location
225                                    };
226
227                                    self.bottom.draw(DrawCtx {
228                                        pdf,
229                                        text_pieces_cache: ctx.text_pieces_cache,
230                                        location: bottom_location,
231                                        width: ctx.width,
232                                        first_height: bottom_height,
233                                        preferred_height: None,
234                                        breakable: None,
235                                    });
236                                }
237                            }
238
239                            break_count = location_idx + 1;
240
241                            current_location = (breakable.do_break)(
242                                pdf,
243                                location_offset + location_idx,
244                                self.height(&common, height),
245                            );
246
247                            current_location.clone()
248                        } else {
249                            (breakable.do_break)(
250                                pdf,
251                                location_offset + location_idx,
252                                self.height(&common, height),
253                            )
254                        };
255
256                        location
257                    },
258                }),
259            })
260        } else {
261            self.content.draw(DrawCtx {
262                pdf: ctx.pdf,
263                text_pieces_cache: ctx.text_pieces_cache,
264                location: ctx.location,
265                width: ctx.width,
266                first_height: common.first_height,
267                preferred_height: ctx.preferred_height.map(|p| p - common.bottom_height),
268                breakable: None,
269            })
270        };
271
272        if let Some((y_offset, bottom_height)) = size
273            .height
274            .map(|h| h + self.gap)
275            .or((!self.collapse).then_some(0.))
276            .zip(common.bottom_size.height)
277        {
278            self.bottom.draw(DrawCtx {
279                pdf: ctx.pdf,
280                text_pieces_cache: ctx.text_pieces_cache,
281                location: Location {
282                    pos: (current_location.pos.0, current_location.pos.1 - y_offset),
283                    ..current_location
284                },
285                width: ctx.width,
286                first_height: bottom_height,
287                preferred_height: None,
288                breakable: None,
289            });
290        }
291
292        self.size(&common, size)
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use crate::{
300        elements::{none::NoneElement, text::Text, titled::Titled},
301        fonts::builtin::BuiltinFont,
302        test_utils::{FranticJumper, binary_snapshots::*},
303    };
304    use insta::*;
305
306    #[test]
307    fn test() {
308        let bytes = test_element_bytes(TestElementParams::breakable(), |mut callback| {
309            let font = BuiltinFont::courier(callback.pdf());
310
311            let content = Text::basic(LOREM_IPSUM, &font, 32.);
312            let content = content.debug(1);
313
314            let bottom = Text::basic("bottom", &font, 12.);
315            let bottom = bottom.debug(2);
316
317            callback.call(
318                &RepeatBottom {
319                    content,
320                    bottom,
321                    gap: 5.,
322                    collapse: true,
323                }
324                .debug(0),
325            );
326        });
327        assert_binary_snapshot!(".pdf", bytes);
328    }
329
330    #[test]
331    fn test_collapse() {
332        let bytes = test_element_bytes(TestElementParams::breakable(), |mut callback| {
333            let font = BuiltinFont::courier(callback.pdf());
334
335            let content = NoneElement;
336            let content = content.debug(1);
337
338            let bottom = Text::basic("bottom", &font, 12.);
339            let bottom = bottom.debug(2);
340
341            callback.call(
342                &RepeatBottom {
343                    content,
344                    bottom,
345                    gap: 5.,
346                    collapse: true,
347                }
348                .debug(0),
349            );
350        });
351        assert_binary_snapshot!(".pdf", bytes);
352    }
353
354    #[test]
355    fn test_no_collapse() {
356        let bytes = test_element_bytes(TestElementParams::breakable(), |mut callback| {
357            let font = BuiltinFont::courier(callback.pdf());
358
359            let content = NoneElement;
360            let content = content.debug(1);
361
362            let bottom = Text::basic("bottom", &font, 12.);
363            let bottom = bottom.debug(2);
364
365            callback.call(
366                &RepeatBottom {
367                    content,
368                    bottom,
369                    gap: 5.,
370                    collapse: false,
371                }
372                .debug(0),
373            );
374        });
375        assert_binary_snapshot!(".pdf", bytes);
376    }
377
378    #[test]
379    fn test_no_collapse_bottom_overflow() {
380        let bytes = test_element_bytes(
381            TestElementParams {
382                first_height: 1.,
383                ..TestElementParams::breakable()
384            },
385            |mut callback| {
386                let font = BuiltinFont::courier(callback.pdf());
387
388                let content = NoneElement;
389                let content = content.debug(1);
390
391                let bottom = Text::basic("bottom", &font, 12.);
392                let bottom = bottom.debug(2);
393
394                callback.call(
395                    &RepeatBottom {
396                        content,
397                        bottom,
398                        gap: 5.,
399                        collapse: false,
400                    }
401                    .debug(0),
402                );
403            },
404        );
405        assert_binary_snapshot!(".pdf", bytes);
406    }
407
408    #[test]
409    fn test_multipage_no_collapse() {
410        let bytes = test_element_bytes(TestElementParams::breakable(), |mut callback| {
411            let font = BuiltinFont::courier(callback.pdf());
412
413            let content = FranticJumper {
414                jumps: vec![(0, None), (0, None), (2, Some(32.)), (3, Some(55.))],
415                size: ElementSize {
416                    width: Some(12.),
417                    height: None,
418                },
419            };
420            let content = content.debug(1);
421
422            let bottom = Text::basic("bottom", &font, 12.);
423            let bottom = bottom.debug(2);
424
425            callback.call(
426                &RepeatBottom {
427                    content,
428                    bottom,
429                    gap: 10.,
430                    collapse: false,
431                }
432                .debug(0),
433            );
434        });
435        assert_binary_snapshot!(".pdf", bytes);
436    }
437
438    #[test]
439    fn test_multipage_collapse() {
440        let bytes = test_element_bytes(TestElementParams::breakable(), |mut callback| {
441            let font = BuiltinFont::courier(callback.pdf());
442
443            let content = FranticJumper {
444                jumps: vec![(1, None), (1, None), (3, Some(32.)), (4, None)],
445                size: ElementSize {
446                    width: Some(12.),
447                    height: None,
448                },
449            };
450            let content = content.debug(1);
451
452            let bottom = Text::basic("bottom", &font, 12.);
453            let bottom = bottom.debug(2);
454
455            callback.call(
456                &RepeatBottom {
457                    content,
458                    bottom,
459                    gap: 10.,
460                    collapse: true,
461                }
462                .debug(0),
463            );
464        });
465        assert_binary_snapshot!(".pdf", bytes);
466    }
467
468    #[test]
469    fn test_titled() {
470        let bytes = test_element_bytes(
471            TestElementParams {
472                first_height: 10.,
473                ..TestElementParams::breakable()
474            },
475            |mut callback| {
476                let font = BuiltinFont::courier(callback.pdf());
477                let title = Text::basic("title", &font, 12.);
478                let title = title.debug(1);
479
480                let content = Text::basic("content", &font, 32.);
481                let content = content.debug(3);
482
483                let bottom = Text::basic("bottom", &font, 12.);
484                let bottom = bottom.debug(4);
485
486                let repeat_bottom = RepeatBottom {
487                    content,
488                    bottom,
489                    gap: 5.,
490                    collapse: true,
491                };
492                let repeat_bottom = repeat_bottom.debug(2);
493
494                callback.call(
495                    &Titled {
496                        title,
497                        content: repeat_bottom,
498                        gap: 5.,
499                        collapse_on_empty_content: true,
500                    }
501                    .debug(0),
502                );
503            },
504        );
505        assert_binary_snapshot!(".pdf", bytes);
506    }
507}