Skip to main content

laser_pdf/elements/
changing_title.rs

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