Skip to main content

laser_pdf/elements/
repeat_after_break.rs

1use crate::{
2    utils::{add_optional_size_with_gap, max_optional_size},
3    *,
4};
5
6pub struct RepeatAfterBreak<'a, T: Element, C: Element> {
7    pub title: &'a T,
8    pub content: &'a C,
9    pub gap: f32,
10    pub collapse_on_empty_content: bool,
11}
12
13impl<'a, T: Element, C: Element> Element for RepeatAfterBreak<'a, T, C> {
14    fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
15        let title_size = self.title.measure(MeasureCtx {
16            text_pieces_cache: ctx.text_pieces_cache,
17            width: ctx.width,
18            first_height: ctx.full_height,
19            breakable: None,
20        });
21
22        let collapse = self.collapse_on_empty_content || title_size.height.is_none();
23
24        if !collapse && ctx.first_height == ctx.full_height {
25            return FirstLocationUsage::WillUse;
26        }
27
28        let y_offset = self.y_offset(title_size);
29        let first_location_usage = self.content.first_location_usage(FirstLocationUsageCtx {
30            text_pieces_cache: ctx.text_pieces_cache,
31            width: ctx.width,
32            first_height: ctx.first_height - y_offset,
33            full_height: ctx.full_height,
34        });
35
36        if collapse && first_location_usage == FirstLocationUsage::NoneHeight {
37            FirstLocationUsage::NoneHeight
38        } else if ctx.first_height < ctx.full_height
39            && (y_offset > ctx.first_height || first_location_usage == FirstLocationUsage::WillSkip)
40        {
41            FirstLocationUsage::WillSkip
42        } else {
43            FirstLocationUsage::WillUse
44        }
45    }
46
47    fn measure(&self, ctx: MeasureCtx) -> ElementSize {
48        let title_size = self.title.measure(MeasureCtx {
49            text_pieces_cache: ctx.text_pieces_cache,
50            width: ctx.width,
51            first_height: ctx
52                .breakable
53                .as_ref()
54                .map(|b| b.full_height)
55                .unwrap_or(ctx.first_height),
56            breakable: None,
57        });
58        let y_offset = self.y_offset(title_size);
59
60        let mut break_count = 0;
61
62        let content_size;
63
64        if let Some(breakable) = ctx.breakable {
65            let first_height;
66            let full_height = breakable.full_height - y_offset;
67
68            if ctx.first_height < breakable.full_height
69                && (y_offset > ctx.first_height || {
70                    let first_location_usage =
71                        self.content.first_location_usage(FirstLocationUsageCtx {
72                            text_pieces_cache: ctx.text_pieces_cache,
73                            width: ctx.width,
74                            first_height: ctx.first_height - y_offset,
75                            full_height: breakable.full_height,
76                        });
77
78                    first_location_usage == FirstLocationUsage::WillSkip
79                })
80            {
81                first_height = full_height;
82                *breakable.break_count = 1;
83            } else {
84                first_height = ctx.first_height - y_offset;
85            }
86
87            content_size = self.content.measure(MeasureCtx {
88                text_pieces_cache: ctx.text_pieces_cache,
89                width: ctx.width,
90                first_height,
91                breakable: Some(BreakableMeasure {
92                    full_height,
93                    break_count: &mut break_count,
94                    extra_location_min_height: breakable.extra_location_min_height,
95                }),
96            });
97
98            *breakable.break_count += break_count;
99        } else {
100            content_size = self.content.measure(MeasureCtx {
101                text_pieces_cache: ctx.text_pieces_cache,
102                width: ctx.width,
103                first_height: ctx.first_height - y_offset,
104                breakable: None,
105            });
106        };
107
108        self.size(
109            title_size,
110            content_size,
111            self.collapse(break_count, content_size),
112        )
113    }
114
115    fn draw(&self, ctx: DrawCtx) -> ElementSize {
116        let title_first_height = ctx
117            .breakable
118            .as_ref()
119            .map(|b| b.full_height)
120            .unwrap_or(ctx.first_height);
121        let title_size = self.title.measure(MeasureCtx {
122            text_pieces_cache: ctx.text_pieces_cache,
123            width: ctx.width,
124            first_height: title_first_height,
125            breakable: None,
126        });
127        let y_offset = self.y_offset(title_size);
128
129        let content_size;
130        let location;
131        let mut last_location_idx = 0;
132
133        if let Some(breakable) = ctx.breakable {
134            let first_height;
135            let location_offset;
136            let full_height = breakable.full_height - y_offset;
137
138            if ctx.first_height < breakable.full_height
139                && (y_offset > ctx.first_height || {
140                    let first_location_usage =
141                        self.content.first_location_usage(FirstLocationUsageCtx {
142                            text_pieces_cache: ctx.text_pieces_cache,
143                            width: ctx.width,
144                            first_height: ctx.first_height - y_offset,
145                            full_height: breakable.full_height,
146                        });
147
148                    first_location_usage == FirstLocationUsage::WillSkip
149                })
150            {
151                first_height = full_height;
152                location = (breakable.do_break)(ctx.pdf, 0, None);
153                location_offset = 1;
154            } else {
155                first_height = ctx.first_height - y_offset;
156                location = ctx.location;
157                location_offset = 0;
158            }
159
160            content_size = self.content.draw(DrawCtx {
161                pdf: ctx.pdf,
162                text_pieces_cache: ctx.text_pieces_cache,
163                location: Location {
164                    pos: (location.pos.0, location.pos.1 - y_offset),
165                    ..location
166                },
167                width: ctx.width,
168                first_height,
169                preferred_height: None,
170                breakable: Some(BreakableDraw {
171                    full_height,
172                    preferred_height_break_count: 0,
173
174                    do_break: &mut |pdf, location_idx, height| {
175                        let mut new_location = (breakable.do_break)(
176                            pdf,
177                            location_idx + location_offset,
178                            add_optional_size_with_gap(height, title_size.height, self.gap),
179                        );
180
181                        if last_location_idx <= location_idx {
182                            for i in last_location_idx + 1..=location_idx {
183                                let location =
184                                    (breakable.do_break)(pdf, i + location_offset - 1, None);
185
186                                self.title.draw(DrawCtx {
187                                    pdf,
188                                    text_pieces_cache: ctx.text_pieces_cache,
189                                    location,
190                                    width: ctx.width,
191                                    first_height: title_first_height,
192                                    preferred_height: None,
193                                    breakable: None,
194                                });
195                            }
196
197                            self.title.draw(DrawCtx {
198                                pdf,
199                                text_pieces_cache: ctx.text_pieces_cache,
200                                location: new_location.clone(),
201                                width: ctx.width,
202                                first_height: title_first_height,
203                                preferred_height: None,
204                                breakable: None,
205                            });
206
207                            last_location_idx = location_idx + 1;
208                        }
209
210                        new_location.pos.1 -= y_offset;
211                        new_location
212                    },
213                }),
214            });
215        } else {
216            location = ctx.location;
217            content_size = self.content.draw(DrawCtx {
218                pdf: ctx.pdf,
219                text_pieces_cache: ctx.text_pieces_cache,
220                location: Location {
221                    pos: (location.pos.0, location.pos.1 - y_offset),
222                    ..location
223                },
224                width: ctx.width,
225                first_height: ctx.first_height - y_offset,
226                preferred_height: None,
227                breakable: None,
228            });
229        };
230
231        let collapse = self.collapse(last_location_idx, content_size);
232
233        // if there were any breaks the title was drawn there
234        if !collapse {
235            self.title.draw(DrawCtx {
236                pdf: ctx.pdf,
237                text_pieces_cache: ctx.text_pieces_cache,
238                location: location.clone(),
239                width: ctx.width,
240                first_height: title_first_height,
241                preferred_height: None,
242                breakable: None,
243            });
244        }
245
246        self.size(title_size, content_size, collapse)
247    }
248}
249
250impl<'a, T: Element, C: Element> RepeatAfterBreak<'a, T, C> {
251    fn y_offset(&self, title_size: ElementSize) -> f32 {
252        title_size.height.map(|h| h + self.gap).unwrap_or(0.)
253    }
254
255    fn collapse(&self, break_count: u32, content_size: ElementSize) -> bool {
256        self.collapse_on_empty_content && break_count == 0 && content_size.height.is_none()
257    }
258
259    fn size(
260        &self,
261        title_size: ElementSize,
262        content_size: ElementSize,
263        collapse: bool,
264    ) -> ElementSize {
265        ElementSize {
266            width: if collapse {
267                content_size.width
268            } else {
269                max_optional_size(title_size.width, content_size.width)
270            },
271            height: if collapse {
272                None
273            } else {
274                add_optional_size_with_gap(title_size.height, content_size.height, self.gap)
275            },
276        }
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::{
284        elements::{force_break::ForceBreak, none::NoneElement, rectangle::Rectangle},
285        test_utils::{
286            build_element::BuildElementCtx,
287            record_passes::{Break, DrawPass, RecordPasses},
288            *,
289        },
290    };
291
292    #[test]
293    fn test_collapse() {
294        for configuration in (ElementTestParams {
295            first_height: 5.,
296            width: 10.,
297            full_height: 10.,
298            pos: (1., 10.),
299            ..Default::default()
300        })
301        .configurations()
302        {
303            let element = RepeatAfterBreak {
304                gap: 1.,
305                collapse_on_empty_content: true,
306                title: &Rectangle {
307                    size: (1., 2.),
308                    fill: None,
309                    outline: None,
310                },
311                content: &NoneElement,
312            };
313
314            let output = configuration.run(&element);
315            output.assert_no_breaks().assert_size(ElementSize {
316                width: None,
317                height: None,
318            });
319        }
320    }
321
322    #[test]
323    fn test_pull_down() {
324        let gap = 1.;
325
326        for configuration in (ElementTestParams {
327            first_height: 5.,
328            width: 10.,
329            full_height: 10.,
330            pos: (1., 10.),
331            ..Default::default()
332        })
333        .configurations()
334        {
335            let element = BuildElement(|BuildElementCtx { pass, .. }, callback| {
336                let title = RecordPasses::new(Rectangle {
337                    size: (2.5, 2.),
338                    fill: None,
339                    outline: None,
340                });
341
342                let content = RecordPasses::new(Rectangle {
343                    size: (2., 3.),
344                    fill: None,
345                    outline: None,
346                });
347
348                let ret = callback.call(RepeatAfterBreak {
349                    gap,
350                    title: &title,
351                    content: &content,
352                    collapse_on_empty_content: false,
353                });
354
355                title.assert_measure_count(1);
356                title.assert_first_location_usage_count(0);
357
358                content.assert_first_location_usage_count(
359                    if configuration.breakable && configuration.use_first_height {
360                        1
361                    } else {
362                        0
363                    },
364                );
365
366                match pass {
367                    build_element::Pass::FirstLocationUsage { .. } => todo!(),
368                    build_element::Pass::Measure { .. } => {
369                        title.assert_draw_count(0);
370                        content.assert_draw_count(0);
371                        content.assert_measure_count(1);
372                    }
373                    build_element::Pass::Draw { .. } => {
374                        let width = WidthConstraint {
375                            max: 10.,
376                            expand: configuration.expand_width,
377                        };
378
379                        let first_height = if configuration.use_first_height {
380                            5.
381                        } else {
382                            10.
383                        };
384
385                        title.assert_draw(DrawPass {
386                            width,
387                            first_height: if configuration.breakable {
388                                10.
389                            } else {
390                                first_height
391                            },
392                            preferred_height: None,
393                            page: if configuration.breakable && configuration.use_first_height {
394                                1
395                            } else {
396                                0
397                            },
398                            layer: 0,
399                            pos: (1., 10.),
400                            breakable: None,
401                        });
402
403                        content.assert_draw(DrawPass {
404                            width,
405                            first_height: if configuration.breakable {
406                                7.
407                            } else {
408                                first_height - 3.
409                            },
410                            preferred_height: None,
411                            page: if configuration.breakable && configuration.use_first_height {
412                                1
413                            } else {
414                                0
415                            },
416                            layer: 0,
417                            pos: (1., 7.),
418                            breakable: if configuration.breakable {
419                                Some(record_passes::BreakableDraw {
420                                    full_height: 7.,
421                                    preferred_height_break_count: 0,
422                                    breaks: vec![],
423                                })
424                            } else {
425                                None
426                            },
427                        });
428                        content.assert_measure_count(0);
429                    }
430                }
431
432                ret
433            });
434
435            let output = configuration.run(&element);
436
437            output.assert_size(ElementSize {
438                width: Some(2.5),
439                height: Some(6.),
440            });
441
442            if let Some(b) = output.breakable {
443                if configuration.use_first_height {
444                    b.assert_break_count(1);
445                } else {
446                    b.assert_break_count(0);
447                }
448            }
449        }
450    }
451
452    #[test]
453    fn test_title_overflow() {
454        let gap = 1.;
455
456        for configuration in (ElementTestParams {
457            first_height: 2.,
458            width: 10.,
459            full_height: 10.,
460            pos: (1., 10.),
461            ..Default::default()
462        })
463        .configurations()
464        {
465            let element = BuildElement(|BuildElementCtx { pass, .. }, callback| {
466                let title = RecordPasses::new(Rectangle {
467                    size: (2.5, 3.),
468                    fill: None,
469                    outline: None,
470                });
471
472                let content = RecordPasses::new(ForceBreak);
473
474                let ret = callback.call(RepeatAfterBreak {
475                    gap,
476                    title: &title,
477                    content: &content,
478                    collapse_on_empty_content: false,
479                });
480
481                title.assert_measure_count(1);
482                title.assert_first_location_usage_count(0);
483
484                content.assert_first_location_usage_count(0);
485
486                match pass {
487                    build_element::Pass::FirstLocationUsage { .. } => todo!(),
488                    build_element::Pass::Measure { .. } => {
489                        title.assert_draw_count(0);
490                        content.assert_draw_count(0);
491                        content.assert_measure_count(1);
492                    }
493                    build_element::Pass::Draw { .. } => {
494                        let width = WidthConstraint {
495                            max: 10.,
496                            expand: configuration.expand_width,
497                        };
498
499                        let first_height = if configuration.use_first_height {
500                            2.
501                        } else {
502                            10.
503                        };
504
505                        if configuration.breakable {
506                            title.assert_draws(&[
507                                DrawPass {
508                                    width,
509                                    first_height: 10.,
510                                    preferred_height: None,
511                                    page: if configuration.use_first_height { 2 } else { 1 },
512                                    layer: 0,
513                                    pos: (1., 10.),
514                                    breakable: None,
515                                },
516                                DrawPass {
517                                    width,
518                                    first_height: 10.,
519                                    preferred_height: None,
520                                    page: if configuration.use_first_height { 1 } else { 0 },
521                                    layer: 0,
522                                    pos: (1., 10.),
523                                    breakable: None,
524                                },
525                            ]);
526                        } else {
527                            title.assert_draw(DrawPass {
528                                width,
529                                first_height,
530                                preferred_height: None,
531                                page: 0,
532                                layer: 0,
533                                pos: (1., 10.),
534                                breakable: None,
535                            });
536                        }
537
538                        content.assert_draw(DrawPass {
539                            width,
540                            first_height: if configuration.breakable {
541                                6.
542                            } else {
543                                first_height - 4.
544                            },
545                            preferred_height: None,
546                            page: if configuration.breakable && configuration.use_first_height {
547                                1
548                            } else {
549                                0
550                            },
551                            layer: 0,
552                            pos: (1., 6.),
553                            breakable: if configuration.breakable {
554                                Some(record_passes::BreakableDraw {
555                                    full_height: 6.,
556                                    preferred_height_break_count: 0,
557                                    breaks: vec![Break {
558                                        page: if configuration.use_first_height { 2 } else { 1 },
559                                        layer: 0,
560                                        pos: (1., 6.),
561                                    }],
562                                })
563                            } else {
564                                None
565                            },
566                        });
567                        content.assert_measure_count(0);
568                    }
569                }
570
571                ret
572            });
573
574            let output = configuration.run(&element);
575
576            output.assert_size(ElementSize {
577                width: Some(2.5),
578                height: Some(3.),
579            });
580
581            if let Some(b) = output.breakable {
582                if configuration.use_first_height {
583                    b.assert_break_count(2);
584                } else {
585                    b.assert_break_count(1);
586                }
587            }
588        }
589    }
590
591    #[test]
592    fn test_unhelpful_breaks() {
593        let gap = 1.;
594
595        for configuration in (ElementTestParams {
596            first_height: 5.,
597            width: 10.,
598            full_height: 10.,
599            pos: (1., 10.),
600            ..Default::default()
601        })
602        .configurations()
603        {
604            let element = BuildElement(|BuildElementCtx { pass, .. }, callback| {
605                let title = RecordPasses::new(Rectangle {
606                    size: (2.5, 5.),
607                    fill: None,
608                    outline: None,
609                });
610
611                let content = RecordPasses::new(Rectangle {
612                    size: (4., 10.),
613                    fill: None,
614                    outline: None,
615                });
616
617                let ret = callback.call(RepeatAfterBreak {
618                    gap,
619                    title: &title,
620                    content: &content,
621                    collapse_on_empty_content: false,
622                });
623
624                title.assert_measure_count(1);
625                title.assert_first_location_usage_count(0);
626
627                content.assert_first_location_usage_count(0);
628
629                match pass {
630                    build_element::Pass::FirstLocationUsage { .. } => todo!(),
631                    build_element::Pass::Measure { .. } => {
632                        title.assert_draw_count(0);
633                        content.assert_draw_count(0);
634                        content.assert_measure_count(1);
635                    }
636                    build_element::Pass::Draw { .. } => {
637                        let width = WidthConstraint {
638                            max: 10.,
639                            expand: configuration.expand_width,
640                        };
641
642                        let first_height = if configuration.use_first_height {
643                            5.
644                        } else {
645                            10.
646                        };
647
648                        title.assert_draw(DrawPass {
649                            width,
650                            first_height: if configuration.breakable {
651                                10.
652                            } else {
653                                first_height
654                            },
655                            preferred_height: None,
656                            page: if configuration.breakable && configuration.use_first_height {
657                                1
658                            } else {
659                                0
660                            },
661                            layer: 0,
662                            pos: (1., 10.),
663                            breakable: None,
664                        });
665
666                        content.assert_draw(DrawPass {
667                            width,
668                            first_height: if configuration.breakable {
669                                4.
670                            } else {
671                                first_height - 6.
672                            },
673                            preferred_height: None,
674
675                            // if the first height is equal to the full height a break won't
676                            // accomplish but if the first height is less we always break if
677                            // first_location_usage is WillSkip because otherwise we'd have to
678                            // call first_location_usage twice
679                            page: if configuration.breakable && configuration.use_first_height {
680                                1
681                            } else {
682                                0
683                            },
684
685                            layer: 0,
686                            pos: (1., 4.),
687                            breakable: if configuration.breakable {
688                                Some(record_passes::BreakableDraw {
689                                    full_height: 4.,
690                                    preferred_height_break_count: 0,
691                                    breaks: vec![],
692                                })
693                            } else {
694                                None
695                            },
696                        });
697                        content.assert_measure_count(0);
698                    }
699                }
700
701                ret
702            });
703
704            let output = configuration.run(&element);
705
706            output.assert_size(ElementSize {
707                width: Some(4.),
708                height: Some(16.),
709            });
710
711            if let Some(b) = output.breakable {
712                if configuration.use_first_height {
713                    b.assert_break_count(1);
714                } else {
715                    b.assert_break_count(0);
716                }
717            }
718        }
719    }
720
721    #[test]
722    fn test_skipped_locations() {
723        let gap = 0.;
724
725        let element = BuildElement(|BuildElementCtx { pass, .. }, callback| {
726            let title = RecordPasses::new(Rectangle {
727                size: (2.5, 5.),
728                fill: None,
729                outline: None,
730            });
731
732            let content = RecordPasses::new(FranticJumper {
733                jumps: vec![(0, Some(0.)), (1, Some(11.)), (4, Some(11.))],
734                size: ElementSize {
735                    width: None,
736                    height: Some(11.),
737                },
738            });
739
740            let ret = callback.call(RepeatAfterBreak {
741                gap,
742                title: &title,
743                content: &content,
744                collapse_on_empty_content: false,
745            });
746
747            title.assert_measure_count(1);
748            title.assert_first_location_usage_count(0);
749
750            content.assert_first_location_usage_count(1);
751
752            match pass {
753                build_element::Pass::FirstLocationUsage { .. } => todo!(),
754                build_element::Pass::Measure { .. } => {
755                    title.assert_draw_count(0);
756                    content.assert_draw_count(0);
757                    content.assert_measure_count(1);
758                }
759                build_element::Pass::Draw { .. } => {
760                    let width = WidthConstraint {
761                        max: 10.,
762                        expand: false,
763                    };
764
765                    let mut draws = (0..=5)
766                        .map(|i| DrawPass {
767                            width,
768                            first_height: 12.,
769                            preferred_height: None,
770                            page: i,
771                            layer: 0,
772                            pos: (1., 20.),
773                            breakable: None,
774                        })
775                        .collect::<Vec<_>>();
776                    draws.rotate_left(1);
777                    title.assert_draws(&draws);
778
779                    content.assert_draw(DrawPass {
780                        width,
781                        first_height: 3.,
782                        preferred_height: None,
783
784                        page: 0,
785
786                        layer: 0,
787                        pos: (1., 15.),
788                        breakable: Some(record_passes::BreakableDraw {
789                            full_height: 7.,
790                            preferred_height_break_count: 0,
791                            breaks: [1, 2, 5]
792                                .into_iter()
793                                .map(|i| record_passes::Break {
794                                    page: i,
795                                    layer: 0,
796                                    pos: (1., 15.),
797                                })
798                                .collect::<Vec<_>>(),
799                        }),
800                    });
801                    content.assert_measure_count(0);
802                }
803            }
804
805            ret
806        });
807
808        let output = test_measure_draw_compatibility(
809            &element,
810            WidthConstraint {
811                max: 10.,
812                expand: false,
813            },
814            8.,
815            Some(12.),
816            (1., 20.),
817            (400., 400.),
818        );
819
820        output.assert_size(ElementSize {
821            width: Some(2.5),
822            height: Some(16.),
823        });
824        output.breakable.unwrap().assert_break_count(5);
825    }
826}