Skip to main content

laser_pdf/elements/
title_or_break.rs

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