Skip to main content

laser_pdf/elements/
break_list.rs

1use crate::{utils::max_optional_size, *};
2
3use self::utils::add_optional_size_with_gap;
4
5pub struct BreakList<C: Fn(BreakListContent) -> Option<()>> {
6    pub gap: f32,
7    pub content: C,
8}
9
10impl<C: Fn(BreakListContent) -> Option<()>> Element for BreakList<C> {
11    fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
12        FirstLocationUsage::WillUse
13    }
14
15    fn measure(&self, mut ctx: MeasureCtx) -> ElementSize {
16        let mut max_width = None;
17        let mut x_offset = None;
18        let mut y_offset = None;
19        let mut line_height = None;
20
21        (self.content)(BreakListContent {
22            pass: Pass::Measure {
23                breakable: ctx.breakable.as_mut(),
24            },
25            text_pieces_cache: ctx.text_pieces_cache,
26            gap: self.gap,
27            width_constraint: ctx.width,
28            height_available: ctx.first_height,
29            max_width: &mut max_width,
30            x_offset: &mut x_offset,
31            y_offset: &mut y_offset,
32            line_height: &mut line_height,
33        });
34
35        ElementSize {
36            width: if ctx.width.expand {
37                Some(ctx.width.max)
38            } else {
39                max_optional_size(max_width, x_offset)
40            },
41            height: match (y_offset, line_height) {
42                (None, None) => None,
43                (None, Some(x)) | (Some(x), None) => Some(x),
44                (Some(y_offset), Some(line_height)) => Some(y_offset + self.gap + line_height),
45            },
46        }
47    }
48
49    fn draw(&self, mut ctx: DrawCtx) -> ElementSize {
50        let mut max_width = None;
51        let mut x_offset = None;
52        let mut y_offset = None;
53        let mut line_height = None;
54
55        (self.content)(BreakListContent {
56            pass: Pass::Draw {
57                pdf: ctx.pdf,
58                location: ctx.location,
59                breakable: ctx.breakable.as_mut().map(|b| (b, 0)),
60            },
61            text_pieces_cache: ctx.text_pieces_cache,
62            gap: self.gap,
63            width_constraint: ctx.width,
64            height_available: ctx.first_height,
65            max_width: &mut max_width,
66            x_offset: &mut x_offset,
67            y_offset: &mut y_offset,
68            line_height: &mut line_height,
69        });
70
71        ElementSize {
72            width: if ctx.width.expand {
73                Some(ctx.width.max)
74            } else {
75                max_optional_size(max_width, x_offset)
76            },
77            height: match (y_offset, line_height) {
78                (None, None) => None,
79                (None, Some(x)) | (Some(x), None) => Some(x),
80                (Some(y_offset), Some(line_height)) => Some(y_offset + self.gap + line_height),
81            },
82        }
83    }
84}
85
86pub struct BreakListContent<'a, 'b, 'c> {
87    pass: Pass<'a, 'b, 'c>,
88
89    text_pieces_cache: &'a TextPiecesCache,
90
91    gap: f32,
92
93    width_constraint: WidthConstraint,
94
95    height_available: f32,
96
97    max_width: &'a mut Option<f32>,
98    x_offset: &'a mut Option<f32>,
99    y_offset: &'a mut Option<f32>,
100    line_height: &'a mut Option<f32>,
101}
102
103enum Pass<'a, 'b, 'c> {
104    FirstLocationUsage {},
105    Measure {
106        breakable: Option<&'a mut BreakableMeasure<'b>>,
107    },
108    Draw {
109        pdf: &'c mut Pdf,
110        breakable: Option<(&'a mut BreakableDraw<'b>, u32)>,
111        location: Location,
112    },
113}
114
115impl<'a, 'b, 'c> BreakListContent<'a, 'b, 'c> {
116    pub fn add<E: Element>(mut self, element: &E) -> Option<Self> {
117        let width_constraint = WidthConstraint {
118            max: self.width_constraint.max,
119            expand: false,
120        };
121
122        let full_height = match self.pass {
123            Pass::FirstLocationUsage { .. } => todo!(),
124            Pass::Measure { ref breakable } => breakable.as_ref().map(|b| b.full_height),
125            Pass::Draw {
126                pdf: &mut ref mut pdf,
127                ref breakable,
128                ..
129            } => breakable.as_ref().map(|b| b.0.full_height),
130        };
131
132        let element_size = element.measure(MeasureCtx {
133            text_pieces_cache: self.text_pieces_cache,
134            width: width_constraint,
135
136            // In the unbreakable case this will be more height than is actually available except
137            // for the first row. The issue is that what row we're in can depend on the width of the
138            // element. And we want to avoid measuring twice. This basically means that elements
139            // that expand to first_height are not supported in a BreakList that is in an
140            // unbreakable context. This seems like an acceptable tradeoff. It might actually make
141            // sense to just not have first_height in unbreakable contexts.
142            first_height: full_height.unwrap_or(self.height_available),
143
144            breakable: None,
145        });
146
147        // line breaking
148        if let (Some(x_offset), Some(width)) = (&mut *self.x_offset, element_size.width) {
149            if *x_offset + self.gap + width > self.width_constraint.max {
150                *self.max_width = max_optional_size(*self.max_width, Some(*x_offset));
151                *self.x_offset = None;
152
153                *self.y_offset = match (*self.y_offset, *self.line_height) {
154                    (None, None) => None,
155                    (None, Some(x)) | (Some(x), None) => Some(x),
156                    (Some(y_offset), Some(line_height)) => Some(y_offset + self.gap + line_height),
157                };
158
159                *self.line_height = None;
160            }
161        }
162
163        let break_needed =
164            if let (Some(full_height), Some(height)) = (full_height, element_size.height) {
165                let y_offset = self.y_offset.map(|y| y + self.gap).unwrap_or(0.);
166
167                y_offset + height > self.height_available
168                    && (y_offset > 0. || full_height > self.height_available)
169            } else {
170                false
171            };
172
173        match self.pass {
174            Pass::Measure {
175                ref mut breakable, ..
176            } => {
177                if break_needed {
178                    *self.x_offset = None;
179                    *self.y_offset = None;
180                    let breakable = breakable.as_deref_mut().unwrap();
181                    *breakable.break_count += 1;
182                    self.height_available = breakable.full_height;
183                }
184            }
185            Pass::Draw {
186                pdf: &mut ref mut pdf,
187                ref mut breakable,
188                ref mut location,
189            } => {
190                if break_needed {
191                    let &mut (&mut ref mut breakable, ref mut location_idx) =
192                        breakable.as_mut().unwrap();
193                    *location = (breakable.do_break)(
194                        pdf,
195                        *location_idx,
196                        add_optional_size_with_gap(*self.y_offset, *self.line_height, self.gap),
197                    );
198                    *self.x_offset = None;
199                    *self.y_offset = None;
200                    self.height_available = breakable.full_height;
201                    *location_idx += 1;
202                }
203
204                let x_offset = if let &mut Some(x_offset) = self.x_offset {
205                    x_offset + self.gap
206                } else {
207                    0.
208                };
209                let y_offset = self.y_offset.map(|y| y + self.gap).unwrap_or(0.);
210
211                element.draw(DrawCtx {
212                    pdf,
213                    text_pieces_cache: self.text_pieces_cache,
214                    location: Location {
215                        pos: (location.pos.0 + x_offset, location.pos.1 - y_offset),
216                        ..*location
217                    },
218
219                    // should we only give it the remaining width here?
220                    // the thing is that we've already measured with that width constraint so it
221                    // should only use as much as it did in measure.
222                    width: width_constraint,
223                    first_height: element_size.height.unwrap_or(0.),
224                    preferred_height: None,
225                    breakable: None,
226                });
227            }
228            _ => todo!(),
229        }
230
231        // at this point all breaking has been done so we should be able to just add the size
232        if let Pass::Measure { .. } | Pass::Draw { .. } = self.pass {
233            *self.x_offset = match (*self.x_offset, element_size.width) {
234                (None, None) => None,
235                (None, Some(x)) | (Some(x), None) => Some(x),
236                (Some(x_offset), Some(width)) => Some(x_offset + self.gap + width),
237            };
238
239            *self.line_height = max_optional_size(*self.line_height, element_size.height);
240        }
241
242        Some(self)
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use crate::{
250        elements::{none::NoneElement, rectangle::Rectangle},
251        test_utils::{
252            assert_passes::{AssertPasses, Pass},
253            build_element::BuildElementCtx,
254            *,
255        },
256    };
257
258    #[test]
259    fn test_empty() {
260        let element = BreakList {
261            gap: 12.,
262            content: |_content| None,
263        };
264
265        for output in ElementTestParams::default().run(&element) {
266            output.assert_size(ElementSize {
267                width: if output.width.expand {
268                    Some(output.width.max)
269                } else {
270                    None
271                },
272                height: None,
273            });
274
275            if let Some(b) = output.breakable {
276                b.assert_break_count(0);
277                b.assert_extra_location_min_height(None);
278            }
279        }
280    }
281
282    #[test]
283    fn test_none() {
284        for configuration in (ElementTestParams {
285            first_height: 1.,
286            width: 1.,
287            full_height: 2.,
288            ..Default::default()
289        })
290        .configurations()
291        {
292            let element = BuildElement(|BuildElementCtx { pass, .. }, callback| {
293                let width = WidthConstraint {
294                    max: 1.,
295                    expand: false,
296                };
297
298                let measure_pass = Pass::Measure {
299                    width,
300                    first_height: if configuration.breakable || !configuration.use_first_height {
301                        2.
302                    } else {
303                        1.
304                    },
305                    full_height: None,
306                };
307
308                let draw_pass = Pass::Draw {
309                    width,
310                    first_height: 0.,
311                    breakable: None,
312                    preferred_height: None,
313                    page: 0,
314                    layer: 0,
315                    pos: configuration.params.pos,
316                };
317
318                let child = AssertPasses::new(
319                    NoneElement,
320                    match pass {
321                        build_element::Pass::FirstLocationUsage { .. } => todo!(),
322                        build_element::Pass::Measure { .. } => vec![measure_pass],
323                        build_element::Pass::Draw { .. } => vec![measure_pass, draw_pass],
324                    },
325                );
326
327                let element = BreakList {
328                    gap: 12.,
329                    content: |content| {
330                        content.add(&child);
331
332                        None
333                    },
334                };
335
336                callback.call(element)
337            });
338
339            let output = configuration.run(&element);
340
341            output.assert_size(ElementSize {
342                width: if output.width.expand {
343                    Some(output.width.max)
344                } else {
345                    None
346                },
347                height: None,
348            });
349
350            if let Some(b) = output.breakable {
351                b.assert_break_count(0);
352                b.assert_extra_location_min_height(None);
353            }
354        }
355    }
356
357    #[test]
358    fn test_passes() {
359        let gap = 1.;
360
361        for configuration in (ElementTestParams {
362            first_height: 5.,
363            width: 10.,
364            full_height: 10.,
365            pos: (1., 10.),
366            ..Default::default()
367        })
368        .configurations()
369        {
370            let element = BuildElement(|BuildElementCtx { pass, .. }, callback| {
371                let width = WidthConstraint {
372                    max: 10.,
373                    expand: false,
374                };
375
376                let first_height = if configuration.breakable || !configuration.use_first_height {
377                    10.
378                } else {
379                    5.
380                };
381
382                let child_0 = {
383                    let measure_pass = Pass::Measure {
384                        width,
385                        first_height,
386                        full_height: None,
387                    };
388
389                    let draw_pass = Pass::Draw {
390                        width,
391                        first_height: 0.,
392                        breakable: None,
393                        preferred_height: None,
394                        page: 0,
395                        layer: 0,
396                        pos: (1., 10.),
397                    };
398
399                    AssertPasses::new(
400                        NoneElement,
401                        match pass {
402                            build_element::Pass::FirstLocationUsage { .. } => todo!(),
403                            build_element::Pass::Measure { .. } => vec![measure_pass],
404                            build_element::Pass::Draw { .. } => vec![measure_pass, draw_pass],
405                        },
406                    )
407                };
408
409                let child_1 = {
410                    let measure_pass = Pass::Measure {
411                        width,
412                        first_height,
413                        full_height: None,
414                    };
415
416                    let breaks = configuration.use_first_height && configuration.breakable;
417
418                    let draw_pass = Pass::Draw {
419                        width,
420                        first_height: 6.,
421                        breakable: None,
422                        preferred_height: None,
423                        page: if breaks { 1 } else { 0 },
424                        layer: 0,
425                        pos: (1., 10.),
426                    };
427
428                    AssertPasses::new(
429                        Rectangle {
430                            size: (1., 6.),
431                            fill: None,
432                            outline: None,
433                        },
434                        match pass {
435                            build_element::Pass::FirstLocationUsage { .. } => todo!(),
436                            build_element::Pass::Measure { .. } => vec![measure_pass],
437                            build_element::Pass::Draw { .. } => vec![measure_pass, draw_pass],
438                        },
439                    )
440                };
441
442                let on_page_1 = configuration.use_first_height && configuration.breakable;
443
444                let child_2 = {
445                    let measure_pass = Pass::Measure {
446                        width,
447                        first_height,
448                        full_height: None,
449                    };
450
451                    let draw_pass = Pass::Draw {
452                        width,
453                        first_height: 4.,
454                        breakable: None,
455                        preferred_height: None,
456                        page: if on_page_1 { 1 } else { 0 },
457                        layer: 0,
458                        pos: (1. + 1. + gap, 10.),
459                    };
460
461                    AssertPasses::new(
462                        Rectangle {
463                            size: (7., 4.),
464                            fill: None,
465                            outline: None,
466                        },
467                        match pass {
468                            build_element::Pass::FirstLocationUsage { .. } => todo!(),
469                            build_element::Pass::Measure { .. } => vec![measure_pass],
470                            build_element::Pass::Draw { .. } => vec![measure_pass, draw_pass],
471                        },
472                    )
473                };
474
475                let child_3 = {
476                    let measure_pass = Pass::Measure {
477                        width,
478                        first_height,
479                        full_height: None,
480                    };
481
482                    let draw_pass = Pass::Draw {
483                        width,
484                        first_height: 4.,
485                        breakable: None,
486                        preferred_height: None,
487                        page: if on_page_1 {
488                            2
489                        } else if configuration.breakable {
490                            1
491                        } else {
492                            0
493                        },
494                        layer: 0,
495                        pos: (
496                            1.,
497                            if configuration.breakable {
498                                10.
499                            } else {
500                                10. - 6. - gap
501                            },
502                        ),
503                    };
504
505                    AssertPasses::new(
506                        Rectangle {
507                            size: (1., 4.),
508                            fill: None,
509                            outline: None,
510                        },
511                        match pass {
512                            build_element::Pass::FirstLocationUsage { .. } => todo!(),
513                            build_element::Pass::Measure { .. } => vec![measure_pass],
514                            build_element::Pass::Draw { .. } => vec![measure_pass, draw_pass],
515                        },
516                    )
517                };
518
519                let element = BreakList {
520                    gap,
521                    content: |content| {
522                        content
523                            .add(&child_0)?
524                            .add(&child_1)?
525                            .add(&child_2)?
526                            .add(&child_3)?;
527
528                        None
529                    },
530                };
531
532                callback.call(element)
533            });
534
535            let output = configuration.run(&element);
536
537            output.assert_size(ElementSize {
538                width: if output.width.expand {
539                    Some(output.width.max)
540                } else {
541                    Some(1. + gap + 7.)
542                },
543                height: Some(if configuration.breakable {
544                    4.
545                } else {
546                    6. + gap + 4.
547                }),
548            });
549
550            if let Some(b) = output.breakable {
551                b.assert_break_count(if configuration.use_first_height { 2 } else { 1 });
552                b.assert_extra_location_min_height(None);
553            }
554        }
555    }
556
557    #[test]
558    fn no_unhelpful_breaks() {
559        // If an element overflows the height, but breaking would not help because the next location
560        // / page is not larger then no breaking should happen.
561
562        {
563            let element = BreakList {
564                gap: 1.,
565                content: |content| {
566                    content
567                        .add(&Rectangle {
568                            size: (1., 9.),
569                            fill: None,
570                            outline: None,
571                        })?
572                        .add(&Rectangle {
573                            size: (1., 9.),
574                            fill: None,
575                            outline: None,
576                        })?
577                        .add(&Rectangle {
578                            size: (1., 9.),
579                            fill: None,
580                            outline: None,
581                        })?;
582
583                    None
584                },
585            };
586
587            let output = test_measure_draw_compatibility(
588                &element,
589                WidthConstraint {
590                    max: 3.,
591                    expand: true,
592                },
593                8.,
594                Some(8.),
595                (1., 2.),
596                (0., 0.),
597            );
598
599            output.assert_size(ElementSize {
600                width: Some(3.),
601                height: Some(9.),
602            });
603            output.breakable.unwrap().assert_break_count(1);
604        }
605
606        {
607            // If there's no gap and a row with a height of zero it still is the full height. For
608            // non-zero gaps we could just look at whether the y offset is None (if a
609            // NoneElement was all there is in the previous row for example) and if it isn't just
610            // assume we don't have the full height because the first height can't be more than the
611            // full_heigth. But for a zero gap that optimization doesn't work.
612            let element = BreakList {
613                gap: 0.,
614                content: |content| {
615                    content
616                        .add(&Rectangle {
617                            size: (1., 9.),
618                            fill: None,
619                            outline: None,
620                        })?
621                        .add(&Rectangle {
622                            size: (1.5, 0.),
623                            fill: None,
624                            outline: None,
625                        })?
626                        .add(&Rectangle {
627                            size: (1., 9.),
628                            fill: None,
629                            outline: None,
630                        })?;
631
632                    None
633                },
634            };
635
636            let output = test_measure_draw_compatibility(
637                &element,
638                WidthConstraint {
639                    max: 2.,
640                    expand: false,
641                },
642                8.,
643                Some(8.),
644                (1., 2.),
645                (0., 0.),
646            );
647
648            output.assert_size(ElementSize {
649                width: Some(1.5),
650                height: Some(9.),
651            });
652            output.breakable.unwrap().assert_break_count(1);
653        }
654    }
655}