Skip to main content

laser_pdf/elements/
titled.rs

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