Skip to main content

laser_pdf/elements/
align_preferred_height_bottom.rs

1use crate::*;
2
3pub struct AlignPreferredHeightBottom<E: Element>(pub E);
4
5impl<E: Element> Element for AlignPreferredHeightBottom<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            Some(ctx.full_height),
12            0,
13            0.,
14        );
15
16        if layout.breaks > 0 {
17            FirstLocationUsage::WillSkip
18        } else if layout.size.height.is_some() {
19            FirstLocationUsage::WillUse
20        } else {
21            FirstLocationUsage::NoneHeight
22        }
23    }
24
25    fn measure(&self, mut ctx: MeasureCtx) -> ElementSize {
26        let layout = self.layout(
27            ctx.text_pieces_cache,
28            ctx.width,
29            ctx.first_height,
30            ctx.breakable.as_ref().map(|b| b.full_height),
31            0,
32            0.,
33        );
34
35        if layout.breaks > 0 {
36            let breakable = ctx.breakable.as_mut().unwrap();
37
38            *breakable.break_count = layout.breaks;
39
40            Some(breakable.full_height)
41        } else {
42            layout.size.height.map(|_| ctx.first_height)
43        };
44
45        if let Some(breakable) = ctx.breakable {
46            *breakable.extra_location_min_height = layout.size.height;
47        }
48
49        ElementSize {
50            width: layout.size.width,
51            height: layout.size.height.map(|h| h + layout.y_offset),
52        }
53    }
54
55    fn draw(&self, ctx: DrawCtx) -> ElementSize {
56        let layout = self.layout(
57            ctx.text_pieces_cache,
58            ctx.width,
59            ctx.first_height,
60            ctx.breakable.as_ref().map(|b| b.full_height),
61            ctx.breakable
62                .as_ref()
63                .map(|b| b.preferred_height_break_count)
64                .unwrap_or(0),
65            ctx.preferred_height.unwrap_or(0.),
66        );
67
68        let height_available;
69        let mut location;
70
71        if layout.breaks > 0 {
72            let breakable = ctx.breakable.unwrap();
73
74            location = (breakable.do_break)(ctx.pdf, layout.breaks - 1, None);
75            height_available = breakable.full_height;
76        } else {
77            location = ctx.location;
78            height_available = ctx.first_height;
79        }
80
81        location.pos.1 -= layout.y_offset;
82
83        self.0.draw(DrawCtx {
84            pdf: ctx.pdf,
85            text_pieces_cache: ctx.text_pieces_cache,
86            location,
87            width: ctx.width,
88            first_height: height_available,
89            preferred_height: None,
90            breakable: None,
91        });
92
93        ElementSize {
94            width: layout.size.width,
95            height: layout.size.height.map(|h| h + layout.y_offset),
96        }
97    }
98}
99
100#[derive(Debug)]
101struct Layout {
102    breaks: u32,
103    y_offset: f32,
104    size: ElementSize,
105}
106
107impl<E: Element> AlignPreferredHeightBottom<E> {
108    fn layout(
109        &self,
110        text_pieces_cache: &TextPiecesCache,
111        width: WidthConstraint,
112        first_height: f32,
113        full_height: Option<f32>,
114        preferred_breaks: u32,
115        preferred_height: f32,
116    ) -> Layout {
117        let height_available = full_height.unwrap_or(first_height);
118
119        let size = self.0.measure(MeasureCtx {
120            text_pieces_cache,
121            width,
122            first_height: height_available,
123            breakable: None,
124        });
125
126        let breaks;
127        let location_height;
128
129        if let (Some(height), Some(_)) = (size.height, full_height) {
130            if preferred_breaks == 0 && height > first_height {
131                breaks = 1;
132                location_height = 0.;
133            } else {
134                breaks = preferred_breaks;
135                location_height = preferred_height;
136            }
137        } else {
138            breaks = 0;
139            location_height = preferred_height;
140        };
141
142        let y_offset = if let Some(height) = size.height {
143            (location_height - height).max(0.)
144        } else {
145            0.
146        };
147
148        Layout {
149            breaks,
150            y_offset,
151            size,
152        }
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use crate::{
160        elements::ref_element::RefElement,
161        test_utils::{record_passes::RecordPasses, *},
162    };
163    use insta::*;
164
165    #[test]
166    fn test_unbreakable() {
167        let output = test_element(
168            TestElementParams {
169                width: WidthConstraint {
170                    max: 12.,
171                    expand: true,
172                },
173                first_height: 21.,
174                breakable: None,
175                pos: (11., 29.0),
176                ..Default::default()
177            },
178            |assert, callback| {
179                let content = RecordPasses::new(FakeText {
180                    lines: 3,
181                    line_height: 5.,
182                    width: 3.,
183                });
184
185                let element = AlignPreferredHeightBottom(RefElement(&content));
186
187                let ret = callback.call(element);
188
189                if assert {
190                    assert_debug_snapshot!(content.into_passes());
191                }
192
193                ret
194            },
195        );
196
197        assert_debug_snapshot!(output);
198    }
199
200    #[test]
201    fn test_unbreakable_preferred_height() {
202        let output = test_element(
203            TestElementParams {
204                width: WidthConstraint {
205                    max: 12.,
206                    expand: true,
207                },
208                first_height: 21.,
209                breakable: None,
210                pos: (11., 29.0),
211                preferred_height: Some(17.),
212                ..Default::default()
213            },
214            |assert, callback| {
215                let content = RecordPasses::new(FakeText {
216                    lines: 3,
217                    line_height: 5.,
218                    width: 3.,
219                });
220
221                let element = AlignPreferredHeightBottom(RefElement(&content));
222
223                let ret = callback.call(element);
224
225                if assert {
226                    assert_debug_snapshot!(content.into_passes());
227                }
228
229                ret
230            },
231        );
232
233        assert_debug_snapshot!(output);
234    }
235
236    #[test]
237    fn test_breakable() {
238        let output = test_element(
239            TestElementParams {
240                width: WidthConstraint {
241                    max: 12.,
242                    expand: true,
243                },
244                first_height: 21.,
245                breakable: Some(TestElementParamsBreakable {
246                    full_height: 25.,
247                    ..Default::default()
248                }),
249                pos: (11., 29.0),
250                ..Default::default()
251            },
252            |assert, callback| {
253                let content = RecordPasses::new(FakeText {
254                    lines: 3,
255                    line_height: 5.,
256                    width: 3.,
257                });
258
259                let element = AlignPreferredHeightBottom(RefElement(&content));
260
261                let ret = callback.call(element);
262
263                if assert {
264                    assert_debug_snapshot!(content.into_passes());
265                }
266
267                ret
268            },
269        );
270
271        assert_debug_snapshot!(output);
272    }
273
274    #[test]
275    fn test_pre_break() {
276        let output = test_element(
277            TestElementParams {
278                width: WidthConstraint {
279                    max: 12.,
280                    expand: true,
281                },
282                first_height: 21.,
283                breakable: Some(TestElementParamsBreakable {
284                    full_height: 26.,
285                    ..Default::default()
286                }),
287                pos: (11., 29.0),
288                ..Default::default()
289            },
290            |assert, callback| {
291                let content = RecordPasses::new(FakeText {
292                    lines: 5,
293                    line_height: 5.,
294                    width: 3.,
295                });
296
297                let element = AlignPreferredHeightBottom(RefElement(&content));
298
299                let ret = callback.call(element);
300
301                if assert {
302                    assert_debug_snapshot!(content.into_passes());
303                }
304
305                ret
306            },
307        );
308
309        assert_debug_snapshot!(output);
310    }
311
312    #[test]
313    fn test_pre_break_preferred_height() {
314        let width = WidthConstraint {
315            max: 12.,
316            expand: true,
317        };
318        let first_height = 21.;
319        let full_height = 26.;
320        let pos = (11., 29.0);
321
322        let output = test_element(
323            TestElementParams {
324                width,
325                first_height,
326                breakable: Some(TestElementParamsBreakable {
327                    full_height,
328                    ..Default::default()
329                }),
330                pos,
331                preferred_height: Some(20.),
332                ..Default::default()
333            },
334            |assert, callback| {
335                let content = RecordPasses::new(FakeText {
336                    lines: 5,
337                    line_height: 5.,
338                    width: 3.,
339                });
340
341                let element = AlignPreferredHeightBottom(RefElement(&content));
342
343                let ret = callback.call(element);
344
345                if assert {
346                    assert_debug_snapshot!(content.into_passes());
347                }
348
349                ret
350            },
351        );
352
353        assert_debug_snapshot!(output);
354    }
355
356    #[test]
357    fn test_preferred_breaks() {
358        let output = test_element(
359            TestElementParams {
360                width: WidthConstraint {
361                    max: 12.,
362                    expand: true,
363                },
364                first_height: 21.,
365                breakable: Some(TestElementParamsBreakable {
366                    full_height: 26.,
367                    preferred_height_break_count: 4,
368                }),
369                pos: (11., 29.0),
370                preferred_height: None,
371                ..Default::default()
372            },
373            |assert, callback| {
374                let content = RecordPasses::new(FakeText {
375                    lines: 5,
376                    line_height: 5.,
377                    width: 3.,
378                });
379
380                let element = AlignPreferredHeightBottom(RefElement(&content));
381
382                let ret = callback.call(element);
383
384                if assert {
385                    assert_debug_snapshot!(content.into_passes());
386                }
387
388                ret
389            },
390        );
391
392        assert_debug_snapshot!(output);
393    }
394
395    #[test]
396    fn test_preferred_height() {
397        let output = test_element(
398            TestElementParams {
399                width: WidthConstraint {
400                    max: 12.,
401                    expand: true,
402                },
403                first_height: 21.,
404                breakable: Some(TestElementParamsBreakable {
405                    full_height: 23.,
406                    preferred_height_break_count: 3,
407                }),
408                pos: (11., 29.0),
409                preferred_height: Some(21.5),
410                ..Default::default()
411            },
412            |assert, callback| {
413                let content = RecordPasses::new(FakeText {
414                    lines: 4,
415                    line_height: 5.,
416                    width: 3.,
417                });
418
419                let element = AlignPreferredHeightBottom(RefElement(&content));
420
421                let ret = callback.call(element);
422
423                if assert {
424                    assert_debug_snapshot!(content.into_passes());
425                }
426
427                ret
428            },
429        );
430
431        assert_debug_snapshot!(output);
432    }
433}