Skip to main content

laser_pdf/elements/
align_location_bottom.rs

1use crate::*;
2
3pub struct AlignLocationBottom<E: Element>(pub E);
4
5impl<E: Element> Element for AlignLocationBottom<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        );
14
15        if layout.breaks > 0 {
16            FirstLocationUsage::WillSkip
17        } else if layout.size.height.is_some() {
18            FirstLocationUsage::WillUse
19        } else {
20            FirstLocationUsage::NoneHeight
21        }
22    }
23
24    fn measure(&self, mut ctx: MeasureCtx) -> ElementSize {
25        let layout = self.layout(
26            ctx.text_pieces_cache,
27            ctx.width,
28            ctx.first_height,
29            ctx.breakable.as_ref().map(|b| b.full_height),
30            0,
31        );
32
33        let height = if layout.breaks > 0 {
34            let breakable = ctx.breakable.as_mut().unwrap();
35
36            *breakable.break_count = layout.breaks;
37
38            Some(breakable.full_height)
39        } else {
40            layout.size.height.map(|_| ctx.first_height)
41        };
42
43        if let Some(breakable) = ctx.breakable {
44            *breakable.extra_location_min_height = Some(breakable.full_height);
45        }
46
47        ElementSize {
48            width: layout.size.width,
49            height,
50        }
51    }
52
53    fn draw(&self, ctx: DrawCtx) -> ElementSize {
54        let layout = self.layout(
55            ctx.text_pieces_cache,
56            ctx.width,
57            ctx.first_height,
58            ctx.breakable.as_ref().map(|b| b.full_height),
59            ctx.breakable
60                .as_ref()
61                .map(|b| b.preferred_height_break_count)
62                .unwrap_or(0),
63        );
64
65        let height_available;
66        let height;
67        let mut location;
68
69        if layout.breaks > 0 {
70            let breakable = ctx.breakable.unwrap();
71
72            location = (breakable.do_break)(ctx.pdf, layout.breaks - 1, None);
73            height_available = breakable.full_height;
74
75            height = Some(breakable.full_height);
76        } else {
77            location = ctx.location;
78            height_available = ctx.first_height;
79            height = layout.size.height.map(|_| ctx.first_height);
80        }
81
82        location.pos.1 -= layout.y_offset;
83
84        self.0.draw(DrawCtx {
85            pdf: ctx.pdf,
86            text_pieces_cache: ctx.text_pieces_cache,
87            location,
88            width: ctx.width,
89            first_height: height_available,
90            preferred_height: None,
91            breakable: None,
92        });
93
94        ElementSize {
95            width: layout.size.width,
96            height,
97        }
98    }
99}
100
101#[derive(Debug)]
102struct Layout {
103    breaks: u32,
104    y_offset: f32,
105    size: ElementSize,
106}
107
108impl<E: Element> AlignLocationBottom<E> {
109    fn layout(
110        &self,
111        text_pieces_cache: &TextPiecesCache,
112        width: WidthConstraint,
113        first_height: f32,
114        full_height: Option<f32>,
115        preferred_breaks: u32,
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(full_height)) = (size.height, full_height) {
130            breaks = if preferred_breaks == 0 && height > first_height {
131                1
132            } else {
133                preferred_breaks
134            };
135
136            location_height = if breaks > 0 {
137                full_height
138            } else {
139                first_height
140            };
141        } else {
142            breaks = 0;
143            location_height = first_height;
144        };
145
146        let y_offset = if let Some(height) = size.height {
147            location_height - height
148        } else {
149            0.
150        };
151
152        Layout {
153            breaks,
154            y_offset,
155            size,
156        }
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::{
164        elements::ref_element::RefElement,
165        test_utils::{record_passes::RecordPasses, *},
166    };
167    use insta::*;
168
169    #[test]
170    fn test_unbreakable() {
171        let output = test_element(
172            TestElementParams {
173                width: WidthConstraint {
174                    max: 12.,
175                    expand: true,
176                },
177                first_height: 21.,
178                breakable: None,
179                pos: (11., 29.0),
180                ..Default::default()
181            },
182            |assert, callback| {
183                let content = RecordPasses::new(FakeText {
184                    lines: 3,
185                    line_height: 5.,
186                    width: 3.,
187                });
188
189                let element = AlignLocationBottom(RefElement(&content));
190
191                let ret = callback.call(element);
192
193                if assert {
194                    assert_debug_snapshot!(content.into_passes());
195                }
196
197                ret
198            },
199        );
200
201        assert_debug_snapshot!(output);
202    }
203
204    #[test]
205    fn test_breakable() {
206        let output = test_element(
207            TestElementParams {
208                width: WidthConstraint {
209                    max: 12.,
210                    expand: true,
211                },
212                first_height: 21.,
213                breakable: Some(TestElementParamsBreakable {
214                    full_height: 25.,
215                    ..Default::default()
216                }),
217                pos: (11., 29.0),
218                ..Default::default()
219            },
220            |assert, callback| {
221                let content = RecordPasses::new(FakeText {
222                    lines: 3,
223                    line_height: 5.,
224                    width: 3.,
225                });
226
227                let element = AlignLocationBottom(RefElement(&content));
228
229                let ret = callback.call(element);
230
231                if assert {
232                    assert_debug_snapshot!(content.into_passes());
233                }
234
235                ret
236            },
237        );
238
239        assert_debug_snapshot!(output);
240    }
241
242    #[test]
243    fn test_pre_break() {
244        let output = test_element(
245            TestElementParams {
246                width: WidthConstraint {
247                    max: 12.,
248                    expand: true,
249                },
250                first_height: 21.,
251                breakable: Some(TestElementParamsBreakable {
252                    full_height: 26.,
253                    ..Default::default()
254                }),
255                pos: (11., 29.0),
256                ..Default::default()
257            },
258            |assert, callback| {
259                let content = RecordPasses::new(FakeText {
260                    lines: 5,
261                    line_height: 5.,
262                    width: 3.,
263                });
264
265                let element = AlignLocationBottom(RefElement(&content));
266
267                let ret = callback.call(element);
268
269                if assert {
270                    assert_debug_snapshot!(content.into_passes());
271                }
272
273                ret
274            },
275        );
276
277        assert_debug_snapshot!(output);
278    }
279
280    #[test]
281    fn test_preferred_height() {
282        let output = test_element(
283            TestElementParams {
284                width: WidthConstraint {
285                    max: 12.,
286                    expand: true,
287                },
288                first_height: 21.,
289                breakable: Some(TestElementParamsBreakable {
290                    full_height: 26.,
291                    preferred_height_break_count: 4,
292                }),
293                pos: (11., 29.0),
294                preferred_height: None,
295                ..Default::default()
296            },
297            |assert, callback| {
298                let content = RecordPasses::new(FakeText {
299                    lines: 5,
300                    line_height: 5.,
301                    width: 3.,
302                });
303
304                let element = AlignLocationBottom(RefElement(&content));
305
306                let ret = callback.call(element);
307
308                if assert {
309                    assert_debug_snapshot!(content.into_passes());
310                }
311
312                ret
313            },
314        );
315
316        assert_debug_snapshot!(output);
317    }
318}