1use crate::{
2 utils::{add_optional_size_with_gap, max_optional_size},
3 *,
4};
5
6pub struct RepeatAfterBreak<'a, T: Element, C: Element> {
7 pub title: &'a T,
8 pub content: &'a C,
9 pub gap: f32,
10 pub collapse_on_empty_content: bool,
11}
12
13impl<'a, T: Element, C: Element> Element for RepeatAfterBreak<'a, T, C> {
14 fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
15 let title_size = self.title.measure(MeasureCtx {
16 text_pieces_cache: ctx.text_pieces_cache,
17 width: ctx.width,
18 first_height: ctx.full_height,
19 breakable: None,
20 });
21
22 let collapse = self.collapse_on_empty_content || title_size.height.is_none();
23
24 if !collapse && ctx.first_height == ctx.full_height {
25 return FirstLocationUsage::WillUse;
26 }
27
28 let y_offset = self.y_offset(title_size);
29 let first_location_usage = self.content.first_location_usage(FirstLocationUsageCtx {
30 text_pieces_cache: ctx.text_pieces_cache,
31 width: ctx.width,
32 first_height: ctx.first_height - y_offset,
33 full_height: ctx.full_height,
34 });
35
36 if collapse && first_location_usage == FirstLocationUsage::NoneHeight {
37 FirstLocationUsage::NoneHeight
38 } else if ctx.first_height < ctx.full_height
39 && (y_offset > ctx.first_height || first_location_usage == FirstLocationUsage::WillSkip)
40 {
41 FirstLocationUsage::WillSkip
42 } else {
43 FirstLocationUsage::WillUse
44 }
45 }
46
47 fn measure(&self, ctx: MeasureCtx) -> ElementSize {
48 let title_size = self.title.measure(MeasureCtx {
49 text_pieces_cache: ctx.text_pieces_cache,
50 width: ctx.width,
51 first_height: ctx
52 .breakable
53 .as_ref()
54 .map(|b| b.full_height)
55 .unwrap_or(ctx.first_height),
56 breakable: None,
57 });
58 let y_offset = self.y_offset(title_size);
59
60 let mut break_count = 0;
61
62 let content_size;
63
64 if let Some(breakable) = ctx.breakable {
65 let first_height;
66 let full_height = breakable.full_height - y_offset;
67
68 if ctx.first_height < breakable.full_height
69 && (y_offset > ctx.first_height || {
70 let first_location_usage =
71 self.content.first_location_usage(FirstLocationUsageCtx {
72 text_pieces_cache: ctx.text_pieces_cache,
73 width: ctx.width,
74 first_height: ctx.first_height - y_offset,
75 full_height: breakable.full_height,
76 });
77
78 first_location_usage == FirstLocationUsage::WillSkip
79 })
80 {
81 first_height = full_height;
82 *breakable.break_count = 1;
83 } else {
84 first_height = ctx.first_height - y_offset;
85 }
86
87 content_size = self.content.measure(MeasureCtx {
88 text_pieces_cache: ctx.text_pieces_cache,
89 width: ctx.width,
90 first_height,
91 breakable: Some(BreakableMeasure {
92 full_height,
93 break_count: &mut break_count,
94 extra_location_min_height: breakable.extra_location_min_height,
95 }),
96 });
97
98 *breakable.break_count += break_count;
99 } else {
100 content_size = self.content.measure(MeasureCtx {
101 text_pieces_cache: ctx.text_pieces_cache,
102 width: ctx.width,
103 first_height: ctx.first_height - y_offset,
104 breakable: None,
105 });
106 };
107
108 self.size(
109 title_size,
110 content_size,
111 self.collapse(break_count, content_size),
112 )
113 }
114
115 fn draw(&self, ctx: DrawCtx) -> ElementSize {
116 let title_first_height = ctx
117 .breakable
118 .as_ref()
119 .map(|b| b.full_height)
120 .unwrap_or(ctx.first_height);
121 let title_size = self.title.measure(MeasureCtx {
122 text_pieces_cache: ctx.text_pieces_cache,
123 width: ctx.width,
124 first_height: title_first_height,
125 breakable: None,
126 });
127 let y_offset = self.y_offset(title_size);
128
129 let content_size;
130 let location;
131 let mut last_location_idx = 0;
132
133 if let Some(breakable) = ctx.breakable {
134 let first_height;
135 let location_offset;
136 let full_height = breakable.full_height - y_offset;
137
138 if ctx.first_height < breakable.full_height
139 && (y_offset > ctx.first_height || {
140 let first_location_usage =
141 self.content.first_location_usage(FirstLocationUsageCtx {
142 text_pieces_cache: ctx.text_pieces_cache,
143 width: ctx.width,
144 first_height: ctx.first_height - y_offset,
145 full_height: breakable.full_height,
146 });
147
148 first_location_usage == FirstLocationUsage::WillSkip
149 })
150 {
151 first_height = full_height;
152 location = (breakable.do_break)(ctx.pdf, 0, None);
153 location_offset = 1;
154 } else {
155 first_height = ctx.first_height - y_offset;
156 location = ctx.location;
157 location_offset = 0;
158 }
159
160 content_size = self.content.draw(DrawCtx {
161 pdf: ctx.pdf,
162 text_pieces_cache: ctx.text_pieces_cache,
163 location: Location {
164 pos: (location.pos.0, location.pos.1 - y_offset),
165 ..location
166 },
167 width: ctx.width,
168 first_height,
169 preferred_height: None,
170 breakable: Some(BreakableDraw {
171 full_height,
172 preferred_height_break_count: 0,
173
174 do_break: &mut |pdf, location_idx, height| {
175 let mut new_location = (breakable.do_break)(
176 pdf,
177 location_idx + location_offset,
178 add_optional_size_with_gap(height, title_size.height, self.gap),
179 );
180
181 if last_location_idx <= location_idx {
182 for i in last_location_idx + 1..=location_idx {
183 let location =
184 (breakable.do_break)(pdf, i + location_offset - 1, None);
185
186 self.title.draw(DrawCtx {
187 pdf,
188 text_pieces_cache: ctx.text_pieces_cache,
189 location,
190 width: ctx.width,
191 first_height: title_first_height,
192 preferred_height: None,
193 breakable: None,
194 });
195 }
196
197 self.title.draw(DrawCtx {
198 pdf,
199 text_pieces_cache: ctx.text_pieces_cache,
200 location: new_location.clone(),
201 width: ctx.width,
202 first_height: title_first_height,
203 preferred_height: None,
204 breakable: None,
205 });
206
207 last_location_idx = location_idx + 1;
208 }
209
210 new_location.pos.1 -= y_offset;
211 new_location
212 },
213 }),
214 });
215 } else {
216 location = ctx.location;
217 content_size = self.content.draw(DrawCtx {
218 pdf: ctx.pdf,
219 text_pieces_cache: ctx.text_pieces_cache,
220 location: Location {
221 pos: (location.pos.0, location.pos.1 - y_offset),
222 ..location
223 },
224 width: ctx.width,
225 first_height: ctx.first_height - y_offset,
226 preferred_height: None,
227 breakable: None,
228 });
229 };
230
231 let collapse = self.collapse(last_location_idx, content_size);
232
233 if !collapse {
235 self.title.draw(DrawCtx {
236 pdf: ctx.pdf,
237 text_pieces_cache: ctx.text_pieces_cache,
238 location: location.clone(),
239 width: ctx.width,
240 first_height: title_first_height,
241 preferred_height: None,
242 breakable: None,
243 });
244 }
245
246 self.size(title_size, content_size, collapse)
247 }
248}
249
250impl<'a, T: Element, C: Element> RepeatAfterBreak<'a, T, C> {
251 fn y_offset(&self, title_size: ElementSize) -> f32 {
252 title_size.height.map(|h| h + self.gap).unwrap_or(0.)
253 }
254
255 fn collapse(&self, break_count: u32, content_size: ElementSize) -> bool {
256 self.collapse_on_empty_content && break_count == 0 && content_size.height.is_none()
257 }
258
259 fn size(
260 &self,
261 title_size: ElementSize,
262 content_size: ElementSize,
263 collapse: bool,
264 ) -> ElementSize {
265 ElementSize {
266 width: if collapse {
267 content_size.width
268 } else {
269 max_optional_size(title_size.width, content_size.width)
270 },
271 height: if collapse {
272 None
273 } else {
274 add_optional_size_with_gap(title_size.height, content_size.height, self.gap)
275 },
276 }
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use crate::{
284 elements::{force_break::ForceBreak, none::NoneElement, rectangle::Rectangle},
285 test_utils::{
286 build_element::BuildElementCtx,
287 record_passes::{Break, DrawPass, RecordPasses},
288 *,
289 },
290 };
291
292 #[test]
293 fn test_collapse() {
294 for configuration in (ElementTestParams {
295 first_height: 5.,
296 width: 10.,
297 full_height: 10.,
298 pos: (1., 10.),
299 ..Default::default()
300 })
301 .configurations()
302 {
303 let element = RepeatAfterBreak {
304 gap: 1.,
305 collapse_on_empty_content: true,
306 title: &Rectangle {
307 size: (1., 2.),
308 fill: None,
309 outline: None,
310 },
311 content: &NoneElement,
312 };
313
314 let output = configuration.run(&element);
315 output.assert_no_breaks().assert_size(ElementSize {
316 width: None,
317 height: None,
318 });
319 }
320 }
321
322 #[test]
323 fn test_pull_down() {
324 let gap = 1.;
325
326 for configuration in (ElementTestParams {
327 first_height: 5.,
328 width: 10.,
329 full_height: 10.,
330 pos: (1., 10.),
331 ..Default::default()
332 })
333 .configurations()
334 {
335 let element = BuildElement(|BuildElementCtx { pass, .. }, callback| {
336 let title = RecordPasses::new(Rectangle {
337 size: (2.5, 2.),
338 fill: None,
339 outline: None,
340 });
341
342 let content = RecordPasses::new(Rectangle {
343 size: (2., 3.),
344 fill: None,
345 outline: None,
346 });
347
348 let ret = callback.call(RepeatAfterBreak {
349 gap,
350 title: &title,
351 content: &content,
352 collapse_on_empty_content: false,
353 });
354
355 title.assert_measure_count(1);
356 title.assert_first_location_usage_count(0);
357
358 content.assert_first_location_usage_count(
359 if configuration.breakable && configuration.use_first_height {
360 1
361 } else {
362 0
363 },
364 );
365
366 match pass {
367 build_element::Pass::FirstLocationUsage { .. } => todo!(),
368 build_element::Pass::Measure { .. } => {
369 title.assert_draw_count(0);
370 content.assert_draw_count(0);
371 content.assert_measure_count(1);
372 }
373 build_element::Pass::Draw { .. } => {
374 let width = WidthConstraint {
375 max: 10.,
376 expand: configuration.expand_width,
377 };
378
379 let first_height = if configuration.use_first_height {
380 5.
381 } else {
382 10.
383 };
384
385 title.assert_draw(DrawPass {
386 width,
387 first_height: if configuration.breakable {
388 10.
389 } else {
390 first_height
391 },
392 preferred_height: None,
393 page: if configuration.breakable && configuration.use_first_height {
394 1
395 } else {
396 0
397 },
398 layer: 0,
399 pos: (1., 10.),
400 breakable: None,
401 });
402
403 content.assert_draw(DrawPass {
404 width,
405 first_height: if configuration.breakable {
406 7.
407 } else {
408 first_height - 3.
409 },
410 preferred_height: None,
411 page: if configuration.breakable && configuration.use_first_height {
412 1
413 } else {
414 0
415 },
416 layer: 0,
417 pos: (1., 7.),
418 breakable: if configuration.breakable {
419 Some(record_passes::BreakableDraw {
420 full_height: 7.,
421 preferred_height_break_count: 0,
422 breaks: vec![],
423 })
424 } else {
425 None
426 },
427 });
428 content.assert_measure_count(0);
429 }
430 }
431
432 ret
433 });
434
435 let output = configuration.run(&element);
436
437 output.assert_size(ElementSize {
438 width: Some(2.5),
439 height: Some(6.),
440 });
441
442 if let Some(b) = output.breakable {
443 if configuration.use_first_height {
444 b.assert_break_count(1);
445 } else {
446 b.assert_break_count(0);
447 }
448 }
449 }
450 }
451
452 #[test]
453 fn test_title_overflow() {
454 let gap = 1.;
455
456 for configuration in (ElementTestParams {
457 first_height: 2.,
458 width: 10.,
459 full_height: 10.,
460 pos: (1., 10.),
461 ..Default::default()
462 })
463 .configurations()
464 {
465 let element = BuildElement(|BuildElementCtx { pass, .. }, callback| {
466 let title = RecordPasses::new(Rectangle {
467 size: (2.5, 3.),
468 fill: None,
469 outline: None,
470 });
471
472 let content = RecordPasses::new(ForceBreak);
473
474 let ret = callback.call(RepeatAfterBreak {
475 gap,
476 title: &title,
477 content: &content,
478 collapse_on_empty_content: false,
479 });
480
481 title.assert_measure_count(1);
482 title.assert_first_location_usage_count(0);
483
484 content.assert_first_location_usage_count(0);
485
486 match pass {
487 build_element::Pass::FirstLocationUsage { .. } => todo!(),
488 build_element::Pass::Measure { .. } => {
489 title.assert_draw_count(0);
490 content.assert_draw_count(0);
491 content.assert_measure_count(1);
492 }
493 build_element::Pass::Draw { .. } => {
494 let width = WidthConstraint {
495 max: 10.,
496 expand: configuration.expand_width,
497 };
498
499 let first_height = if configuration.use_first_height {
500 2.
501 } else {
502 10.
503 };
504
505 if configuration.breakable {
506 title.assert_draws(&[
507 DrawPass {
508 width,
509 first_height: 10.,
510 preferred_height: None,
511 page: if configuration.use_first_height { 2 } else { 1 },
512 layer: 0,
513 pos: (1., 10.),
514 breakable: None,
515 },
516 DrawPass {
517 width,
518 first_height: 10.,
519 preferred_height: None,
520 page: if configuration.use_first_height { 1 } else { 0 },
521 layer: 0,
522 pos: (1., 10.),
523 breakable: None,
524 },
525 ]);
526 } else {
527 title.assert_draw(DrawPass {
528 width,
529 first_height,
530 preferred_height: None,
531 page: 0,
532 layer: 0,
533 pos: (1., 10.),
534 breakable: None,
535 });
536 }
537
538 content.assert_draw(DrawPass {
539 width,
540 first_height: if configuration.breakable {
541 6.
542 } else {
543 first_height - 4.
544 },
545 preferred_height: None,
546 page: if configuration.breakable && configuration.use_first_height {
547 1
548 } else {
549 0
550 },
551 layer: 0,
552 pos: (1., 6.),
553 breakable: if configuration.breakable {
554 Some(record_passes::BreakableDraw {
555 full_height: 6.,
556 preferred_height_break_count: 0,
557 breaks: vec![Break {
558 page: if configuration.use_first_height { 2 } else { 1 },
559 layer: 0,
560 pos: (1., 6.),
561 }],
562 })
563 } else {
564 None
565 },
566 });
567 content.assert_measure_count(0);
568 }
569 }
570
571 ret
572 });
573
574 let output = configuration.run(&element);
575
576 output.assert_size(ElementSize {
577 width: Some(2.5),
578 height: Some(3.),
579 });
580
581 if let Some(b) = output.breakable {
582 if configuration.use_first_height {
583 b.assert_break_count(2);
584 } else {
585 b.assert_break_count(1);
586 }
587 }
588 }
589 }
590
591 #[test]
592 fn test_unhelpful_breaks() {
593 let gap = 1.;
594
595 for configuration in (ElementTestParams {
596 first_height: 5.,
597 width: 10.,
598 full_height: 10.,
599 pos: (1., 10.),
600 ..Default::default()
601 })
602 .configurations()
603 {
604 let element = BuildElement(|BuildElementCtx { pass, .. }, callback| {
605 let title = RecordPasses::new(Rectangle {
606 size: (2.5, 5.),
607 fill: None,
608 outline: None,
609 });
610
611 let content = RecordPasses::new(Rectangle {
612 size: (4., 10.),
613 fill: None,
614 outline: None,
615 });
616
617 let ret = callback.call(RepeatAfterBreak {
618 gap,
619 title: &title,
620 content: &content,
621 collapse_on_empty_content: false,
622 });
623
624 title.assert_measure_count(1);
625 title.assert_first_location_usage_count(0);
626
627 content.assert_first_location_usage_count(0);
628
629 match pass {
630 build_element::Pass::FirstLocationUsage { .. } => todo!(),
631 build_element::Pass::Measure { .. } => {
632 title.assert_draw_count(0);
633 content.assert_draw_count(0);
634 content.assert_measure_count(1);
635 }
636 build_element::Pass::Draw { .. } => {
637 let width = WidthConstraint {
638 max: 10.,
639 expand: configuration.expand_width,
640 };
641
642 let first_height = if configuration.use_first_height {
643 5.
644 } else {
645 10.
646 };
647
648 title.assert_draw(DrawPass {
649 width,
650 first_height: if configuration.breakable {
651 10.
652 } else {
653 first_height
654 },
655 preferred_height: None,
656 page: if configuration.breakable && configuration.use_first_height {
657 1
658 } else {
659 0
660 },
661 layer: 0,
662 pos: (1., 10.),
663 breakable: None,
664 });
665
666 content.assert_draw(DrawPass {
667 width,
668 first_height: if configuration.breakable {
669 4.
670 } else {
671 first_height - 6.
672 },
673 preferred_height: None,
674
675 page: if configuration.breakable && configuration.use_first_height {
680 1
681 } else {
682 0
683 },
684
685 layer: 0,
686 pos: (1., 4.),
687 breakable: if configuration.breakable {
688 Some(record_passes::BreakableDraw {
689 full_height: 4.,
690 preferred_height_break_count: 0,
691 breaks: vec![],
692 })
693 } else {
694 None
695 },
696 });
697 content.assert_measure_count(0);
698 }
699 }
700
701 ret
702 });
703
704 let output = configuration.run(&element);
705
706 output.assert_size(ElementSize {
707 width: Some(4.),
708 height: Some(16.),
709 });
710
711 if let Some(b) = output.breakable {
712 if configuration.use_first_height {
713 b.assert_break_count(1);
714 } else {
715 b.assert_break_count(0);
716 }
717 }
718 }
719 }
720
721 #[test]
722 fn test_skipped_locations() {
723 let gap = 0.;
724
725 let element = BuildElement(|BuildElementCtx { pass, .. }, callback| {
726 let title = RecordPasses::new(Rectangle {
727 size: (2.5, 5.),
728 fill: None,
729 outline: None,
730 });
731
732 let content = RecordPasses::new(FranticJumper {
733 jumps: vec![(0, Some(0.)), (1, Some(11.)), (4, Some(11.))],
734 size: ElementSize {
735 width: None,
736 height: Some(11.),
737 },
738 });
739
740 let ret = callback.call(RepeatAfterBreak {
741 gap,
742 title: &title,
743 content: &content,
744 collapse_on_empty_content: false,
745 });
746
747 title.assert_measure_count(1);
748 title.assert_first_location_usage_count(0);
749
750 content.assert_first_location_usage_count(1);
751
752 match pass {
753 build_element::Pass::FirstLocationUsage { .. } => todo!(),
754 build_element::Pass::Measure { .. } => {
755 title.assert_draw_count(0);
756 content.assert_draw_count(0);
757 content.assert_measure_count(1);
758 }
759 build_element::Pass::Draw { .. } => {
760 let width = WidthConstraint {
761 max: 10.,
762 expand: false,
763 };
764
765 let mut draws = (0..=5)
766 .map(|i| DrawPass {
767 width,
768 first_height: 12.,
769 preferred_height: None,
770 page: i,
771 layer: 0,
772 pos: (1., 20.),
773 breakable: None,
774 })
775 .collect::<Vec<_>>();
776 draws.rotate_left(1);
777 title.assert_draws(&draws);
778
779 content.assert_draw(DrawPass {
780 width,
781 first_height: 3.,
782 preferred_height: None,
783
784 page: 0,
785
786 layer: 0,
787 pos: (1., 15.),
788 breakable: Some(record_passes::BreakableDraw {
789 full_height: 7.,
790 preferred_height_break_count: 0,
791 breaks: [1, 2, 5]
792 .into_iter()
793 .map(|i| record_passes::Break {
794 page: i,
795 layer: 0,
796 pos: (1., 15.),
797 })
798 .collect::<Vec<_>>(),
799 }),
800 });
801 content.assert_measure_count(0);
802 }
803 }
804
805 ret
806 });
807
808 let output = test_measure_draw_compatibility(
809 &element,
810 WidthConstraint {
811 max: 10.,
812 expand: false,
813 },
814 8.,
815 Some(12.),
816 (1., 20.),
817 (400., 400.),
818 );
819
820 output.assert_size(ElementSize {
821 width: Some(2.5),
822 height: Some(16.),
823 });
824 output.breakable.unwrap().assert_break_count(5);
825 }
826}