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