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