1use crate::*;
2
3use self::utils::{add_optional_size, max_optional_size};
4
5pub struct RepeatBottom<C: Element, B: Element> {
6 pub content: C,
7 pub bottom: 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> RepeatBottom<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.bottom.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 height(&self, common: &Common, height: Option<f32>) -> Option<f32> {
75 height
76 .map(|h| h + self.gap)
77 .or((!self.collapse).then_some(0.))
78 .and_then(|h| add_optional_size(Some(h), common.bottom_size.height))
79 }
80
81 fn size(&self, common: &Common, content_size: ElementSize) -> ElementSize {
82 ElementSize {
83 width: max_optional_size(content_size.width, common.bottom_size.width),
84 height: self.height(common, content_size.height),
85 }
86 }
87}
88
89impl<C: Element, B: Element> Element for RepeatBottom<C, B> {
90 fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
91 let common = self.common(
92 ctx.text_pieces_cache,
93 ctx.width,
94 ctx.first_height,
95 Some(ctx.full_height),
96 );
97
98 if common.pre_break {
99 return FirstLocationUsage::WillSkip;
100 }
101
102 let first_location_usage = common.content_first_location_usage.unwrap_or_else(|| {
103 self.content.first_location_usage(FirstLocationUsageCtx {
104 text_pieces_cache: ctx.text_pieces_cache,
105 width: ctx.width,
106 first_height: common.first_height,
107 full_height: common.full_height.unwrap(),
108 })
109 });
110
111 if first_location_usage == FirstLocationUsage::NoneHeight && !self.collapse {
112 if common.bottom_size.height.is_none() {
113 FirstLocationUsage::NoneHeight
114 } else {
115 FirstLocationUsage::WillUse
116 }
117 } else {
118 first_location_usage
119 }
120 }
121
122 fn measure(&self, mut ctx: MeasureCtx) -> ElementSize {
123 let common = self.common(
124 ctx.text_pieces_cache,
125 ctx.width,
126 ctx.first_height,
127 ctx.breakable.as_ref().map(|b| b.full_height),
128 );
129
130 let mut break_count = 0;
131 let mut extra_location_min_height = None;
132
133 let size = self.content.measure(MeasureCtx {
134 text_pieces_cache: ctx.text_pieces_cache,
135 width: ctx.width,
136 first_height: common.first_height,
137 breakable: ctx.breakable.as_mut().map(|_| BreakableMeasure {
138 full_height: common.full_height.unwrap(),
139 break_count: &mut break_count,
140 extra_location_min_height: &mut extra_location_min_height,
141 }),
142 });
143
144 if let Some(breakable) = ctx.breakable {
145 *breakable.break_count = break_count + u32::from(common.pre_break);
146 *breakable.extra_location_min_height =
147 extra_location_min_height.map(|x| x + common.bottom_height);
148 }
149
150 self.size(&common, size)
151 }
152
153 fn draw(&self, ctx: DrawCtx) -> ElementSize {
154 let common = self.common(
155 ctx.text_pieces_cache,
156 ctx.width,
157 ctx.first_height,
158 ctx.breakable.as_ref().map(|b| b.full_height),
159 );
160
161 let mut current_location = ctx.location.clone();
162
163 let size = if let Some(breakable) = ctx.breakable {
164 let mut break_count = 0;
165
166 let (location, location_offset) = if common.pre_break {
167 current_location = (breakable.do_break)(ctx.pdf, 0, None);
168 (current_location.clone(), 1)
169 } else {
170 (ctx.location, 0)
171 };
172
173 self.content.draw(DrawCtx {
174 pdf: ctx.pdf,
175 text_pieces_cache: ctx.text_pieces_cache,
176 location,
177 width: ctx.width,
178 first_height: common.first_height,
179 preferred_height: ctx.preferred_height.map(|p| p - common.bottom_height),
180 breakable: Some(BreakableDraw {
181 full_height: common.full_height.unwrap(),
182 preferred_height_break_count: breakable.preferred_height_break_count,
183 do_break: &mut |pdf, location_idx, height| {
184 let location = if location_idx >= break_count {
185 if let Some(bottom_height) = common
186 .bottom_size
187 .height
188 .filter(|_| height.is_some() || !self.collapse)
189 {
190 let first_location_idx = if self.collapse {
191 location_idx
192 } else {
193 break_count
194 };
195
196 for i in first_location_idx..=location_idx {
199 let bottom_location = if i == break_count {
200 current_location.clone()
201 } else {
202 (breakable.do_break)(
203 pdf,
204 location_offset + i - 1,
205 (!self.collapse).then_some(bottom_height),
208 )
209 };
210
211 let y_offset = if i == location_idx {
212 assert!(height.is_some() || !self.collapse);
213 height.map(|h| h + self.gap).unwrap_or(0.)
214 } else {
215 assert!(!self.collapse);
216 0.
217 };
218
219 let bottom_location = Location {
220 pos: (
221 bottom_location.pos.0,
222 bottom_location.pos.1 - y_offset,
223 ),
224 ..bottom_location
225 };
226
227 self.bottom.draw(DrawCtx {
228 pdf,
229 text_pieces_cache: ctx.text_pieces_cache,
230 location: bottom_location,
231 width: ctx.width,
232 first_height: bottom_height,
233 preferred_height: None,
234 breakable: None,
235 });
236 }
237 }
238
239 break_count = location_idx + 1;
240
241 current_location = (breakable.do_break)(
242 pdf,
243 location_offset + location_idx,
244 self.height(&common, height),
245 );
246
247 current_location.clone()
248 } else {
249 (breakable.do_break)(
250 pdf,
251 location_offset + location_idx,
252 self.height(&common, height),
253 )
254 };
255
256 location
257 },
258 }),
259 })
260 } else {
261 self.content.draw(DrawCtx {
262 pdf: ctx.pdf,
263 text_pieces_cache: ctx.text_pieces_cache,
264 location: ctx.location,
265 width: ctx.width,
266 first_height: common.first_height,
267 preferred_height: ctx.preferred_height.map(|p| p - common.bottom_height),
268 breakable: None,
269 })
270 };
271
272 if let Some((y_offset, bottom_height)) = size
273 .height
274 .map(|h| h + self.gap)
275 .or((!self.collapse).then_some(0.))
276 .zip(common.bottom_size.height)
277 {
278 self.bottom.draw(DrawCtx {
279 pdf: ctx.pdf,
280 text_pieces_cache: ctx.text_pieces_cache,
281 location: Location {
282 pos: (current_location.pos.0, current_location.pos.1 - y_offset),
283 ..current_location
284 },
285 width: ctx.width,
286 first_height: bottom_height,
287 preferred_height: None,
288 breakable: None,
289 });
290 }
291
292 self.size(&common, size)
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use crate::{
300 elements::{none::NoneElement, text::Text, titled::Titled},
301 fonts::builtin::BuiltinFont,
302 test_utils::{FranticJumper, binary_snapshots::*},
303 };
304 use insta::*;
305
306 #[test]
307 fn test() {
308 let bytes = test_element_bytes(TestElementParams::breakable(), |mut callback| {
309 let font = BuiltinFont::courier(callback.pdf());
310
311 let content = Text::basic(LOREM_IPSUM, &font, 32.);
312 let content = content.debug(1);
313
314 let bottom = Text::basic("bottom", &font, 12.);
315 let bottom = bottom.debug(2);
316
317 callback.call(
318 &RepeatBottom {
319 content,
320 bottom,
321 gap: 5.,
322 collapse: true,
323 }
324 .debug(0),
325 );
326 });
327 assert_binary_snapshot!(".pdf", bytes);
328 }
329
330 #[test]
331 fn test_collapse() {
332 let bytes = test_element_bytes(TestElementParams::breakable(), |mut callback| {
333 let font = BuiltinFont::courier(callback.pdf());
334
335 let content = NoneElement;
336 let content = content.debug(1);
337
338 let bottom = Text::basic("bottom", &font, 12.);
339 let bottom = bottom.debug(2);
340
341 callback.call(
342 &RepeatBottom {
343 content,
344 bottom,
345 gap: 5.,
346 collapse: true,
347 }
348 .debug(0),
349 );
350 });
351 assert_binary_snapshot!(".pdf", bytes);
352 }
353
354 #[test]
355 fn test_no_collapse() {
356 let bytes = test_element_bytes(TestElementParams::breakable(), |mut callback| {
357 let font = BuiltinFont::courier(callback.pdf());
358
359 let content = NoneElement;
360 let content = content.debug(1);
361
362 let bottom = Text::basic("bottom", &font, 12.);
363 let bottom = bottom.debug(2);
364
365 callback.call(
366 &RepeatBottom {
367 content,
368 bottom,
369 gap: 5.,
370 collapse: false,
371 }
372 .debug(0),
373 );
374 });
375 assert_binary_snapshot!(".pdf", bytes);
376 }
377
378 #[test]
379 fn test_no_collapse_bottom_overflow() {
380 let bytes = test_element_bytes(
381 TestElementParams {
382 first_height: 1.,
383 ..TestElementParams::breakable()
384 },
385 |mut callback| {
386 let font = BuiltinFont::courier(callback.pdf());
387
388 let content = NoneElement;
389 let content = content.debug(1);
390
391 let bottom = Text::basic("bottom", &font, 12.);
392 let bottom = bottom.debug(2);
393
394 callback.call(
395 &RepeatBottom {
396 content,
397 bottom,
398 gap: 5.,
399 collapse: false,
400 }
401 .debug(0),
402 );
403 },
404 );
405 assert_binary_snapshot!(".pdf", bytes);
406 }
407
408 #[test]
409 fn test_multipage_no_collapse() {
410 let bytes = test_element_bytes(TestElementParams::breakable(), |mut callback| {
411 let font = BuiltinFont::courier(callback.pdf());
412
413 let content = FranticJumper {
414 jumps: vec![(0, None), (0, None), (2, Some(32.)), (3, Some(55.))],
415 size: ElementSize {
416 width: Some(12.),
417 height: None,
418 },
419 };
420 let content = content.debug(1);
421
422 let bottom = Text::basic("bottom", &font, 12.);
423 let bottom = bottom.debug(2);
424
425 callback.call(
426 &RepeatBottom {
427 content,
428 bottom,
429 gap: 10.,
430 collapse: false,
431 }
432 .debug(0),
433 );
434 });
435 assert_binary_snapshot!(".pdf", bytes);
436 }
437
438 #[test]
439 fn test_multipage_collapse() {
440 let bytes = test_element_bytes(TestElementParams::breakable(), |mut callback| {
441 let font = BuiltinFont::courier(callback.pdf());
442
443 let content = FranticJumper {
444 jumps: vec![(1, None), (1, None), (3, Some(32.)), (4, None)],
445 size: ElementSize {
446 width: Some(12.),
447 height: None,
448 },
449 };
450 let content = content.debug(1);
451
452 let bottom = Text::basic("bottom", &font, 12.);
453 let bottom = bottom.debug(2);
454
455 callback.call(
456 &RepeatBottom {
457 content,
458 bottom,
459 gap: 10.,
460 collapse: true,
461 }
462 .debug(0),
463 );
464 });
465 assert_binary_snapshot!(".pdf", bytes);
466 }
467
468 #[test]
469 fn test_titled() {
470 let bytes = test_element_bytes(
471 TestElementParams {
472 first_height: 10.,
473 ..TestElementParams::breakable()
474 },
475 |mut callback| {
476 let font = BuiltinFont::courier(callback.pdf());
477 let title = Text::basic("title", &font, 12.);
478 let title = title.debug(1);
479
480 let content = Text::basic("content", &font, 32.);
481 let content = content.debug(3);
482
483 let bottom = Text::basic("bottom", &font, 12.);
484 let bottom = bottom.debug(4);
485
486 let repeat_bottom = RepeatBottom {
487 content,
488 bottom,
489 gap: 5.,
490 collapse: true,
491 };
492 let repeat_bottom = repeat_bottom.debug(2);
493
494 callback.call(
495 &Titled {
496 title,
497 content: repeat_bottom,
498 gap: 5.,
499 collapse_on_empty_content: true,
500 }
501 .debug(0),
502 );
503 },
504 );
505 assert_binary_snapshot!(".pdf", bytes);
506 }
507}