Skip to main content

laser_pdf/elements/
pin_below.rs

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