Skip to main content

laser_pdf/elements/
break_whole.rs

1use crate::*;
2
3pub struct BreakWhole<'a, E: Element>(pub &'a E);
4
5impl<'a, E: Element> Element for BreakWhole<'a, E> {
6    fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
7        let layout = self.layout(
8            ctx.text_pieces_cache,
9            ctx.width,
10            ctx.first_height,
11            ctx.full_height,
12        );
13
14        match layout {
15            Layout::NoPreBreak => self.0.first_location_usage(ctx),
16            Layout::Other {
17                pre_break, size, ..
18            } => {
19                if pre_break {
20                    FirstLocationUsage::WillSkip
21                } else if size.height.is_none() {
22                    FirstLocationUsage::NoneHeight
23                } else {
24                    // This is correct because if the element wants to skip it would have to break
25                    // and in that case pre_break would be true.
26                    FirstLocationUsage::WillUse
27                }
28            }
29        }
30    }
31
32    fn measure(&self, ctx: MeasureCtx) -> ElementSize {
33        if let Some(breakable) = ctx.breakable {
34            let layout = self.layout(
35                ctx.text_pieces_cache,
36                ctx.width,
37                ctx.first_height,
38                breakable.full_height,
39            );
40
41            match layout {
42                Layout::NoPreBreak => self.0.measure(MeasureCtx {
43                    breakable: Some(breakable),
44                    ..ctx
45                }),
46                Layout::Other {
47                    pre_break,
48                    break_count,
49                    size,
50                } => {
51                    *breakable.break_count = break_count;
52
53                    if pre_break {
54                        *breakable.break_count += 1;
55                    }
56
57                    size
58                }
59            }
60        } else {
61            self.0.measure(ctx)
62        }
63    }
64
65    fn draw(&self, ctx: DrawCtx) -> ElementSize {
66        if let Some(breakable) = ctx.breakable {
67            let layout = self.layout(
68                ctx.text_pieces_cache,
69                ctx.width,
70                ctx.first_height,
71                breakable.full_height,
72            );
73
74            if let Layout::Other {
75                pre_break: true, ..
76            } = layout
77            {
78                let location = (breakable.do_break)(ctx.pdf, 0, None);
79
80                self.0.draw(DrawCtx {
81                    pdf: ctx.pdf,
82                    text_pieces_cache: ctx.text_pieces_cache,
83                    width: ctx.width,
84                    location,
85                    first_height: breakable.full_height,
86                    preferred_height: None,
87                    breakable: Some(BreakableDraw {
88                        full_height: breakable.full_height,
89                        preferred_height_break_count: 0,
90                        do_break: &mut |pdf, location_idx, height| {
91                            (breakable.do_break)(pdf, location_idx + 1, height)
92                        },
93                    }),
94                })
95            } else {
96                self.0.draw(DrawCtx {
97                    breakable: Some(BreakableDraw {
98                        preferred_height_break_count: 0,
99                        ..breakable
100                    }),
101                    preferred_height: None,
102                    ..ctx
103                })
104            }
105        } else {
106            self.0.draw(DrawCtx {
107                preferred_height: None,
108                ..ctx
109            })
110        }
111    }
112}
113
114enum Layout {
115    /// This is a bit awkward, but in the case where first_height equals full height we don't want
116    /// to do an unnecessary measure so we can't return the element size and break count.
117    NoPreBreak,
118    Other {
119        pre_break: bool,
120        break_count: u32,
121        size: ElementSize,
122    },
123}
124
125impl<'a, E: Element> BreakWhole<'a, E> {
126    fn layout(
127        &self,
128        text_pieces_cache: &TextPiecesCache,
129        width: WidthConstraint,
130        first_height: f32,
131        full_height: f32,
132    ) -> Layout {
133        if first_height == full_height {
134            return Layout::NoPreBreak;
135        }
136
137        let mut break_count = 0;
138        let mut extra_location_min_height = None;
139
140        let size = self.0.measure(MeasureCtx {
141            text_pieces_cache,
142            width,
143            first_height: full_height,
144            breakable: Some(BreakableMeasure {
145                full_height,
146                break_count: &mut break_count,
147                extra_location_min_height: &mut extra_location_min_height,
148            }),
149        });
150
151        Layout::Other {
152            pre_break: break_count > 0 || size.height.is_some_and(|h| h > first_height),
153            break_count,
154            size,
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use crate::test_utils::{
163        record_passes::{Break, BreakableDraw, DrawPass, RecordPasses},
164        *,
165    };
166
167    #[test]
168    fn test_unbreakable() {
169        let width = WidthConstraint {
170            max: 3.,
171            expand: false,
172        };
173        let first_height = 12.;
174        let pos = (2., 10.);
175
176        let element = BuildElement(|ctx, callback| {
177            let content = RecordPasses::new(FakeText {
178                lines: 3,
179                line_height: 5.,
180                width: 3.,
181            });
182
183            let element = BreakWhole(&content);
184
185            let ret = callback.call(element);
186
187            content.assert_first_location_usage_count(0);
188
189            match ctx.pass {
190                build_element::Pass::FirstLocationUsage { .. } => unreachable!(),
191                build_element::Pass::Measure { .. } => {
192                    content.assert_measure_count(1);
193                }
194                build_element::Pass::Draw { .. } => {
195                    content.assert_measure_count(0);
196                    content.assert_draw(DrawPass {
197                        width,
198                        first_height,
199                        preferred_height: None,
200                        page: 0,
201                        layer: 0,
202                        pos,
203                        breakable: None,
204                    });
205                }
206            }
207
208            ret
209        });
210
211        let output =
212            test_measure_draw_compatibility(&element, width, first_height, None, pos, (1., 1.));
213
214        output.assert_size(ElementSize {
215            width: Some(3.),
216            height: Some(15.),
217        });
218    }
219
220    #[test]
221    fn test_no_break() {
222        let width = WidthConstraint {
223            max: 3.,
224            expand: false,
225        };
226        let first_height = 12.;
227        let full_height = 20.;
228        let pos = (2., 10.);
229
230        let element = BuildElement(|ctx, callback| {
231            let content = RecordPasses::new(FakeText {
232                lines: 2,
233                line_height: 5.,
234                width: 3.,
235            });
236
237            let element = BreakWhole(&content);
238
239            let ret = callback.call(element);
240
241            content.assert_measure_count(1);
242            content.assert_first_location_usage_count(0);
243
244            match ctx.pass {
245                build_element::Pass::FirstLocationUsage { .. } => unreachable!(),
246                build_element::Pass::Measure { .. } => {}
247                build_element::Pass::Draw { .. } => {
248                    content.assert_draw(DrawPass {
249                        width,
250                        first_height,
251                        preferred_height: None,
252                        page: 0,
253                        layer: 0,
254                        pos,
255                        breakable: Some(BreakableDraw {
256                            full_height,
257                            preferred_height_break_count: 0,
258                            breaks: vec![],
259                        }),
260                    });
261                }
262            }
263
264            ret
265        });
266
267        let output = test_measure_draw_compatibility(
268            &element,
269            width,
270            first_height,
271            Some(full_height),
272            pos,
273            (1., 1.),
274        );
275
276        output.assert_size(ElementSize {
277            width: Some(3.),
278            height: Some(10.),
279        });
280        output.breakable.unwrap().assert_break_count(0);
281    }
282
283    #[test]
284    fn test_break() {
285        let width = WidthConstraint {
286            max: 3.,
287            expand: false,
288        };
289        let first_height = 12.;
290        let full_height = 20.;
291        let pos = (2., 10.);
292
293        let element = BuildElement(|ctx, callback| {
294            let content = RecordPasses::new(FakeText {
295                lines: 3,
296                line_height: 5.,
297                width: 3.,
298            });
299
300            let element = BreakWhole(&content);
301
302            let ret = callback.call(element);
303
304            content.assert_measure_count(1);
305            content.assert_first_location_usage_count(0);
306
307            match ctx.pass {
308                build_element::Pass::FirstLocationUsage { .. } => unreachable!(),
309                build_element::Pass::Measure { .. } => {}
310                build_element::Pass::Draw { .. } => {
311                    content.assert_draw(DrawPass {
312                        width,
313                        first_height: full_height,
314                        preferred_height: None,
315                        page: 1,
316                        layer: 0,
317                        pos,
318                        breakable: Some(BreakableDraw {
319                            full_height,
320                            preferred_height_break_count: 0,
321                            breaks: vec![],
322                        }),
323                    });
324                }
325            }
326
327            ret
328        });
329
330        let output = test_measure_draw_compatibility(
331            &element,
332            width,
333            first_height,
334            Some(full_height),
335            pos,
336            (1., 1.),
337        );
338
339        output.assert_size(ElementSize {
340            width: Some(3.),
341            height: Some(15.),
342        });
343        output.breakable.unwrap().assert_break_count(1);
344    }
345
346    #[test]
347    fn test_unhelpful_break() {
348        let width = WidthConstraint {
349            max: 3.,
350            expand: false,
351        };
352        let first_height = 14.;
353        let full_height = 14.;
354        let pos = (2., 10.);
355
356        let element = BuildElement(|ctx, callback| {
357            let content = RecordPasses::new(FakeText {
358                lines: 3,
359                line_height: 5.,
360                width: 3.,
361            });
362
363            let element = BreakWhole(&content);
364
365            let ret = callback.call(element);
366
367            content.assert_first_location_usage_count(0);
368
369            match ctx.pass {
370                build_element::Pass::FirstLocationUsage { .. } => unreachable!(),
371                build_element::Pass::Measure { .. } => {
372                    content.assert_measure_count(1);
373                }
374                build_element::Pass::Draw { .. } => {
375                    content.assert_measure_count(0);
376                    content.assert_draw(DrawPass {
377                        width,
378                        first_height: full_height,
379                        preferred_height: None,
380                        page: 0,
381                        layer: 0,
382                        pos,
383                        breakable: Some(BreakableDraw {
384                            full_height,
385                            preferred_height_break_count: 0,
386                            breaks: vec![Break {
387                                page: 1,
388                                layer: 0,
389                                pos,
390                            }],
391                        }),
392                    });
393                }
394            }
395
396            ret
397        });
398
399        let output = test_measure_draw_compatibility(
400            &element,
401            width,
402            first_height,
403            Some(full_height),
404            pos,
405            (1., 1.),
406        );
407
408        output.assert_size(ElementSize {
409            width: Some(3.),
410            height: Some(5.),
411        });
412        output.breakable.unwrap().assert_break_count(1);
413    }
414}