Skip to main content

laser_pdf/elements/
min_first_height.rs

1use crate::*;
2
3pub struct MinFirstHeight<E: Element> {
4    pub element: E,
5    pub min_first_height: f32,
6}
7
8impl<E: Element> Element for MinFirstHeight<E> {
9    fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
10        use FirstLocationUsage::*;
11
12        let layout = self.layout(
13            ctx.text_pieces_cache,
14            ctx.width,
15            ctx.first_height,
16            ctx.full_height,
17        );
18
19        if layout.pre_break {
20            match self.element.first_location_usage(FirstLocationUsageCtx {
21                text_pieces_cache: ctx.text_pieces_cache,
22                width: ctx.width,
23                first_height: ctx.full_height,
24                full_height: ctx.full_height,
25            }) {
26                NoneHeight => NoneHeight, // collapse
27                _ => WillSkip,
28            }
29        } else {
30            match layout.measured {
31                Some(measure_output) if measure_output.break_count == 0 => {
32                    if measure_output.size.height.is_none() {
33                        NoneHeight
34                    } else {
35                        WillUse
36                    }
37                }
38                _ => self.element.first_location_usage(ctx),
39            }
40        }
41    }
42
43    fn measure(&self, ctx: MeasureCtx) -> ElementSize {
44        if let Some(breakable) = ctx.breakable {
45            let location_offset;
46            let first_height;
47
48            let layout = self.layout(
49                ctx.text_pieces_cache,
50                ctx.width,
51                ctx.first_height,
52                breakable.full_height,
53            );
54
55            if layout.pre_break {
56                first_height = breakable.full_height;
57                location_offset = 1;
58            } else {
59                first_height = ctx.first_height;
60                location_offset = 0;
61            }
62
63            let size = if let Some(measure_output) = layout.measured {
64                *breakable.break_count = measure_output.break_count;
65                *breakable.extra_location_min_height = measure_output.extra_location_min_height;
66                measure_output.size
67            } else {
68                self.element.measure(MeasureCtx {
69                    text_pieces_cache: ctx.text_pieces_cache,
70                    width: ctx.width,
71                    first_height,
72                    breakable: Some(BreakableMeasure {
73                        full_height: breakable.full_height,
74                        break_count: breakable.break_count,
75                        extra_location_min_height: breakable.extra_location_min_height,
76                    }),
77                })
78            };
79
80            *breakable.break_count += location_offset;
81            size
82        } else {
83            self.element.measure(ctx)
84        }
85    }
86
87    fn draw(&self, ctx: DrawCtx) -> ElementSize {
88        if let Some(breakable) = ctx.breakable {
89            let location;
90            let first_height;
91            let preferred_height;
92            let location_offset;
93
94            if self
95                .layout(
96                    ctx.text_pieces_cache,
97                    ctx.width,
98                    ctx.first_height,
99                    breakable.full_height,
100                )
101                .pre_break
102            {
103                location = (breakable.do_break)(ctx.pdf, 0, None);
104                location_offset = 1;
105                first_height = breakable.full_height;
106                preferred_height = if breakable.preferred_height_break_count == 0 {
107                    None
108                } else {
109                    ctx.preferred_height
110                };
111            } else {
112                location = ctx.location;
113                location_offset = 0;
114                first_height = ctx.first_height;
115                preferred_height = ctx.preferred_height;
116            }
117
118            self.element.draw(DrawCtx {
119                pdf: ctx.pdf,
120                text_pieces_cache: ctx.text_pieces_cache,
121                location,
122                width: ctx.width,
123                first_height,
124                preferred_height,
125                breakable: Some(BreakableDraw {
126                    full_height: breakable.full_height,
127                    preferred_height_break_count: breakable
128                        .preferred_height_break_count
129                        .saturating_sub(location_offset),
130
131                    do_break: &mut |pdf, location_idx, height| {
132                        (breakable.do_break)(pdf, location_idx + location_offset, height)
133                    },
134                }),
135            })
136        } else {
137            self.element.draw(ctx)
138        }
139    }
140}
141
142struct MeasureOutput {
143    size: ElementSize,
144    break_count: u32,
145    extra_location_min_height: Option<f32>,
146}
147
148struct Layout {
149    pre_break: bool,
150    measured: Option<MeasureOutput>,
151}
152
153impl<E: Element> MinFirstHeight<E> {
154    #[inline(always)]
155    fn layout(
156        &self,
157        text_pieces_cache: &TextPiecesCache,
158        width: WidthConstraint,
159        first_height: f32,
160        full_height: f32,
161    ) -> Layout {
162        let mut measured = None;
163        let pre_break = first_height < full_height && first_height < self.min_first_height && {
164            let mut break_count = 0;
165            let mut extra_location_min_height = None;
166
167            let size = self.element.measure(MeasureCtx {
168                text_pieces_cache,
169                width,
170                first_height,
171                breakable: Some(BreakableMeasure {
172                    full_height,
173                    break_count: &mut break_count,
174                    extra_location_min_height: &mut extra_location_min_height,
175                }),
176            });
177
178            if break_count > 0 {
179                true
180            } else {
181                measured = Some(MeasureOutput {
182                    size,
183                    break_count,
184                    extra_location_min_height,
185                });
186                false
187            }
188        };
189
190        Layout {
191            pre_break,
192            measured,
193        }
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::{
201        elements::{none::NoneElement, ref_element::RefElement, text::Text},
202        fonts::builtin::BuiltinFont,
203        test_utils::{record_passes::RecordPasses, *},
204    };
205    use insta::assert_debug_snapshot;
206
207    #[test]
208    fn test_unbreakable() {
209        let output = test_element(
210            TestElementParams {
211                width: WidthConstraint {
212                    max: 12.,
213                    expand: false,
214                },
215                first_height: 12.,
216                breakable: None,
217                pos: (7., 20.0),
218                ..Default::default()
219            },
220            |assert, callback| {
221                let content = RecordPasses::new(FakeText {
222                    lines: 1,
223                    line_height: 5.,
224                    width: 3.,
225                });
226
227                let element = MinFirstHeight {
228                    element: RefElement(&content),
229                    min_first_height: 10.,
230                };
231
232                let ret = callback.call(element);
233
234                if assert {
235                    assert_debug_snapshot!(content.into_passes());
236                }
237
238                ret
239            },
240        );
241
242        assert_debug_snapshot!(output);
243    }
244
245    #[test]
246    fn test_breakable() {
247        let output = test_element(
248            TestElementParams {
249                width: WidthConstraint {
250                    max: 12.,
251                    expand: false,
252                },
253                first_height: 12.,
254                breakable: Some(TestElementParamsBreakable {
255                    preferred_height_break_count: 0,
256                    full_height: 15.,
257                }),
258                pos: (7., 20.0),
259                ..Default::default()
260            },
261            |assert, callback| {
262                let content = RecordPasses::new(FakeText {
263                    lines: 1,
264                    line_height: 5.,
265                    width: 3.,
266                });
267
268                let element = MinFirstHeight {
269                    element: RefElement(&content),
270                    min_first_height: 10.,
271                };
272
273                let ret = callback.call(element);
274
275                if assert {
276                    assert_debug_snapshot!(content.into_passes());
277                }
278
279                ret
280            },
281        );
282
283        assert_debug_snapshot!(output);
284    }
285
286    #[test]
287    fn test_pre_break() {
288        use crate::test_utils::binary_snapshots::*;
289        use insta::*;
290
291        let bytes = test_element_bytes(
292            TestElementParams {
293                first_height: 9.,
294                ..TestElementParams::breakable()
295            },
296            |mut callback| {
297                let font = BuiltinFont::courier(callback.pdf());
298
299                let content = Text::basic(LOREM_IPSUM, &font, 12.);
300                let content = content.debug(1).show_max_width();
301
302                callback.call(
303                    &MinFirstHeight {
304                        element: content,
305                        min_first_height: 10.,
306                    }
307                    .debug(0)
308                    .show_max_width()
309                    .show_last_location_max_height(),
310                );
311            },
312        );
313        assert_binary_snapshot!(".pdf", bytes);
314    }
315
316    #[test]
317    fn test_collapse() {
318        let output = test_element(
319            TestElementParams {
320                width: WidthConstraint {
321                    max: 12.,
322                    expand: false,
323                },
324                first_height: 9.,
325                breakable: Some(TestElementParamsBreakable {
326                    preferred_height_break_count: 3,
327                    full_height: 15.,
328                }),
329                pos: (7., 20.0),
330                preferred_height: Some(4.),
331                ..Default::default()
332            },
333            |assert, callback| {
334                let content = RecordPasses::new(NoneElement);
335
336                let element = MinFirstHeight {
337                    element: RefElement(&content),
338                    min_first_height: 10.,
339                };
340
341                let ret = callback.call(element);
342
343                if assert {
344                    assert_debug_snapshot!(content.into_passes());
345                }
346
347                ret
348            },
349        );
350
351        assert_debug_snapshot!(output);
352    }
353}