1use crate::*;
2
3use self::utils::{add_optional_size, max_optional_size};
4
5pub struct ChangingTitle<F: Element, R: Element, C: Element> {
6 pub first_title: F,
7 pub remaining_title: R,
8 pub content: C,
9 pub gap: f32,
10 pub collapse: bool,
11}
12
13struct CommonBreakable {
14 full_height: f32,
15 pre_break: bool,
16 remaining_title_size: ElementSize,
17 total_remaining_title_height: f32,
18 content_first_location_usage: Option<FirstLocationUsage>,
19}
20
21struct Common {
22 first_height: f32,
23 first_title_size: ElementSize,
24 total_first_title_height: f32,
25 breakable: Option<CommonBreakable>,
26}
27
28impl<F: Element, R: Element, C: Element> ChangingTitle<F, R, C> {
29 fn common(
30 &self,
31 text_pieces_cache: &TextPiecesCache,
32 width: WidthConstraint,
33 first_height: f32,
34 full_height: Option<f32>,
35 ) -> Common {
36 let bottom_first_height = full_height.unwrap_or(first_height);
37
38 let first_title_size = self.first_title.measure(MeasureCtx {
39 text_pieces_cache,
40 width,
41 first_height: bottom_first_height,
42 breakable: None,
43 });
44
45 let total_first_title_height = first_title_size.height.map(|h| h + self.gap).unwrap_or(0.);
46
47 let mut first_height = first_height - total_first_title_height;
48
49 let breakable = full_height.map(|full_height| {
50 let remaining_title_size = self.remaining_title.measure(MeasureCtx {
51 text_pieces_cache,
52 width,
53 first_height: full_height,
54 breakable: None,
55 });
56 let total_remaining_title_height = remaining_title_size
57 .height
58 .map(|h| h + self.gap)
59 .unwrap_or(0.);
60
61 let full_height = full_height - total_remaining_title_height;
62
63 let mut content_first_location_usage = None;
64
65 let pre_break = first_height < full_height
66 && !self.collapse
67 && (first_title_size.height > Some(first_height)
68 || *content_first_location_usage.insert(self.content.first_location_usage(
69 FirstLocationUsageCtx {
70 text_pieces_cache,
71 width,
72 first_height,
73 full_height,
74 },
75 )) == FirstLocationUsage::WillSkip);
76
77 if pre_break {
78 first_height = full_height;
79 } else {
80 first_height = first_height.min(full_height);
82 }
83
84 CommonBreakable {
85 full_height,
86 pre_break,
87 remaining_title_size,
88 total_remaining_title_height,
89 content_first_location_usage,
90 }
91 });
92
93 Common {
94 first_height,
95 first_title_size,
96 total_first_title_height,
97 breakable,
98 }
99 }
100
101 fn height(&self, title_height: Option<f32>, height: Option<f32>) -> Option<f32> {
102 height
103 .map(|h| h + self.gap)
104 .or((!self.collapse).then_some(0.))
105 .and_then(|h| add_optional_size(Some(h), title_height))
106 }
107
108 fn size(&self, common: &Common, break_count: u32, content_size: ElementSize) -> ElementSize {
109 let first_width = max_optional_size(content_size.width, common.first_title_size.width);
110
111 if break_count == 0 {
112 ElementSize {
113 width: first_width,
114 height: self.height(common.first_title_size.height, content_size.height),
115 }
116 } else {
117 let breakable = common.breakable.as_ref().unwrap();
118
119 ElementSize {
120 width: max_optional_size(first_width, breakable.remaining_title_size.width),
121 height: self.height(breakable.remaining_title_size.height, content_size.height),
122 }
123 }
124 }
125}
126
127impl<F: Element, R: Element, C: Element> Element for ChangingTitle<F, R, C> {
128 fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
129 let common = self.common(
130 ctx.text_pieces_cache,
131 ctx.width,
132 ctx.first_height,
133 Some(ctx.full_height),
134 );
135 let breakable = common.breakable.unwrap();
136
137 if breakable.pre_break {
138 return FirstLocationUsage::WillSkip;
139 }
140
141 let first_location_usage = breakable.content_first_location_usage.unwrap_or_else(|| {
142 self.content.first_location_usage(FirstLocationUsageCtx {
143 text_pieces_cache: ctx.text_pieces_cache,
144 width: ctx.width,
145 first_height: common.first_height,
146 full_height: breakable.full_height,
147 })
148 });
149
150 if first_location_usage == FirstLocationUsage::NoneHeight && !self.collapse {
151 if common.first_title_size.height.is_none() {
152 FirstLocationUsage::NoneHeight
153 } else {
154 FirstLocationUsage::WillUse
155 }
156 } else {
157 first_location_usage
158 }
159 }
160
161 fn measure(&self, mut ctx: MeasureCtx) -> ElementSize {
162 let common = self.common(
163 ctx.text_pieces_cache,
164 ctx.width,
165 ctx.first_height,
166 ctx.breakable.as_ref().map(|b| b.full_height),
167 );
168
169 let mut break_count = 0;
170 let mut extra_location_min_height = None;
171
172 let size = self.content.measure(MeasureCtx {
173 text_pieces_cache: ctx.text_pieces_cache,
174 width: ctx.width,
175 first_height: common.first_height,
176 breakable: ctx.breakable.as_mut().zip(common.breakable.as_ref()).map(
177 |(_, breakable)| BreakableMeasure {
178 full_height: breakable.full_height,
179 break_count: &mut break_count,
180 extra_location_min_height: &mut extra_location_min_height,
181 },
182 ),
183 });
184
185 if let Some((breakable, common_breakable)) = ctx.breakable.zip(common.breakable.as_ref()) {
186 *breakable.break_count = break_count + u32::from(common_breakable.pre_break);
187 *breakable.extra_location_min_height = extra_location_min_height
188 .map(|x| x + common_breakable.total_remaining_title_height);
189 }
190
191 self.size(&common, break_count, size)
192 }
193
194 fn draw(&self, ctx: DrawCtx) -> ElementSize {
195 let common = self.common(
196 ctx.text_pieces_cache,
197 ctx.width,
198 ctx.first_height,
199 ctx.breakable.as_ref().map(|b| b.full_height),
200 );
201
202 let mut current_location = ctx.location.clone();
203 let mut break_count = 0;
204
205 let size = if let Some((breakable, common_breakable)) =
206 ctx.breakable.zip(common.breakable.as_ref())
207 {
208 let (location, location_offset) = if common_breakable.pre_break {
209 current_location = (breakable.do_break)(ctx.pdf, 0, None);
210 (current_location.clone(), 1)
211 } else {
212 (ctx.location.clone(), 0)
213 };
214
215 self.content.draw(DrawCtx {
216 pdf: ctx.pdf,
217 text_pieces_cache: ctx.text_pieces_cache,
218 location: Location {
219 pos: (
220 location.pos.0,
221 location.pos.1 - common.total_first_title_height,
222 ),
223 ..location
224 },
225 width: ctx.width,
226 first_height: common.first_height,
227 preferred_height: ctx.preferred_height.map(|p| {
228 p - if breakable.preferred_height_break_count > 0 {
229 common.total_first_title_height
230 } else {
231 common_breakable.total_remaining_title_height
232 }
233 }),
234 breakable: Some(BreakableDraw {
235 full_height: common_breakable.full_height,
236 preferred_height_break_count: breakable.preferred_height_break_count,
237 do_break: &mut |pdf, location_idx, height| {
238 let outer_height = self.height(
239 if location_idx == 0 {
240 common.first_title_size.height
241 } else {
242 common_breakable.remaining_title_size.height
243 },
244 height,
245 );
246
247 let location = if location_idx >= break_count {
248 if let Some(first_height) =
249 common.first_title_size.height.filter(|_| break_count == 0)
250 {
251 self.first_title.draw(DrawCtx {
252 pdf,
253 text_pieces_cache: ctx.text_pieces_cache,
254 location: ctx.location.clone(),
255 width: ctx.width,
256 first_height,
257 preferred_height: None,
258 breakable: None,
259 });
260 }
261
262 if let Some(title_height) =
263 common_breakable.remaining_title_size.height.filter(|_| {
264 (height.is_some() || !self.collapse) && location_idx > 0
265 })
266 {
267 let first_location_idx = if self.collapse {
268 location_idx
269 } else {
270 break_count.max(1)
271 };
272
273 for i in first_location_idx..=location_idx {
276 let title_location = if i == break_count {
277 current_location.clone()
278 } else {
279 (breakable.do_break)(
280 pdf,
281 location_offset + i - 1,
282 (!self.collapse).then_some(title_height),
285 )
286 };
287
288 self.remaining_title.draw(DrawCtx {
289 pdf,
290 text_pieces_cache: ctx.text_pieces_cache,
291 location: title_location,
292 width: ctx.width,
293 first_height: title_height,
294 preferred_height: None,
295 breakable: None,
296 });
297 }
298 }
299
300 break_count = location_idx + 1;
301
302 current_location = (breakable.do_break)(
303 pdf,
304 location_offset + location_idx,
305 outer_height,
306 );
307
308 current_location.clone()
309 } else {
310 (breakable.do_break)(pdf, location_offset + location_idx, outer_height)
311 };
312
313 Location {
314 pos: (
315 location.pos.0,
316 location.pos.1 - common_breakable.total_remaining_title_height,
317 ),
318 ..location
319 }
320 },
321 }),
322 })
323 } else {
324 self.content.draw(DrawCtx {
325 pdf: ctx.pdf,
326 text_pieces_cache: ctx.text_pieces_cache,
327 location: Location {
328 pos: (
329 ctx.location.pos.0,
330 ctx.location.pos.1 - common.total_first_title_height,
331 ),
332 ..ctx.location
333 },
334 width: ctx.width,
335 first_height: common.first_height,
336 preferred_height: ctx
337 .preferred_height
338 .map(|p| p - common.total_first_title_height),
339 breakable: None,
340 })
341 };
342
343 if let Some(title_height) = (if break_count == 0 {
344 common.first_title_size.height
345 } else {
346 common
347 .breakable
348 .as_ref()
349 .unwrap()
350 .remaining_title_size
351 .height
352 })
353 .filter(|_| size.height.is_some() || !self.collapse)
354 {
355 let draw_ctx = DrawCtx {
356 pdf: ctx.pdf,
357 text_pieces_cache: ctx.text_pieces_cache,
358 location: current_location,
359 width: ctx.width,
360 first_height: title_height,
361 preferred_height: None,
362 breakable: None,
363 };
364
365 if break_count == 0 {
366 self.first_title.draw(draw_ctx);
367 } else {
368 self.remaining_title.draw(draw_ctx);
369 }
370 }
371
372 self.size(&common, break_count, size)
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use crate::{
380 elements::{none::NoneElement, text::Text, titled::Titled},
381 fonts::builtin::BuiltinFont,
382 test_utils::{FranticJumper, binary_snapshots::*},
383 };
384 use insta::*;
385
386 #[test]
387 fn test() {
388 let bytes = test_element_bytes(TestElementParams::breakable(), |mut callback| {
389 let font = BuiltinFont::courier(callback.pdf());
390
391 let first = Text::basic("first", &font, 12.);
392 let first = first.debug(1);
393
394 let remaining = Text::basic("remaining\nremaining", &font, 12.);
395 let remaining = remaining.debug(2);
396
397 let content = Text::basic(LOREM_IPSUM, &font, 32.);
398 let content = content.debug(3);
399
400 callback.call(
401 &ChangingTitle {
402 first_title: first,
403 remaining_title: remaining,
404 content,
405 gap: 5.,
406 collapse: true,
407 }
408 .debug(0),
409 );
410 });
411 assert_binary_snapshot!(".pdf", bytes);
412 }
413
414 #[test]
415 fn test_first_height_not_greater_than_full_height() {
416 let bytes = test_element_bytes(
417 TestElementParams {
418 first_height: TestElementParams::DEFAULT_FULL_HEIGHT,
419 ..TestElementParams::breakable()
420 },
421 |mut callback| {
422 let font = BuiltinFont::courier(callback.pdf());
423
424 let first = Text::basic("first", &font, 12.);
425 let first = first.debug(1);
426
427 let remaining = Text::basic("remaining\nremaining", &font, 12.);
428 let remaining = remaining.debug(2);
429
430 let content = Text::basic(LOREM_IPSUM, &font, 48.);
431 let content = content.debug(3);
432
433 callback.call(
434 &ChangingTitle {
435 first_title: first,
436 remaining_title: remaining,
437 content,
438 gap: 5.,
439 collapse: true,
440 }
441 .debug(0),
442 );
443 },
444 );
445 assert_binary_snapshot!(".pdf", bytes);
446 }
447
448 #[test]
449 fn test_collapse() {
450 let bytes = test_element_bytes(TestElementParams::unbreakable(), |mut callback| {
451 let font = BuiltinFont::courier(callback.pdf());
452
453 let first = Text::basic("first", &font, 12.);
454 let first = first.debug(1);
455
456 let remaining = Text::basic("remaining\nremaining", &font, 12.);
457 let remaining = remaining.debug(2);
458
459 let content = NoneElement;
460 let content = content.debug(3);
461
462 callback.call(
463 &ChangingTitle {
464 first_title: first,
465 remaining_title: remaining,
466 content,
467 gap: 5.,
468 collapse: true,
469 }
470 .debug(0),
471 );
472 });
473 assert_binary_snapshot!(".pdf", bytes);
474 }
475
476 #[test]
477 fn test_no_collapse() {
478 let bytes = test_element_bytes(TestElementParams::unbreakable(), |mut callback| {
479 let font = BuiltinFont::courier(callback.pdf());
480
481 let first = Text::basic("first", &font, 12.);
482 let first = first.debug(1);
483
484 let remaining = Text::basic("remaining\nremaining", &font, 12.);
485 let remaining = remaining.debug(2);
486
487 let content = NoneElement;
488 let content = content.debug(3);
489
490 callback.call(
491 &ChangingTitle {
492 first_title: first,
493 remaining_title: remaining,
494 content,
495 gap: 5.,
496 collapse: false,
497 }
498 .debug(0),
499 );
500 });
501 assert_binary_snapshot!(".pdf", bytes);
502 }
503
504 #[test]
505 fn test_multipage_collapse() {
506 let bytes = test_element_bytes(TestElementParams::breakable(), |mut callback| {
507 let font = BuiltinFont::courier(callback.pdf());
508
509 let first = Text::basic("first", &font, 12.);
510 let first = first.debug(1);
511
512 let remaining = Text::basic("remaining\nremaining", &font, 12.);
513 let remaining = remaining.debug(2);
514
515 let content = FranticJumper {
516 jumps: vec![(1, None), (1, None), (3, Some(32.)), (4, None)],
517 size: ElementSize {
518 width: Some(44.),
519 height: None,
520 },
521 };
522 let content = content.debug(3);
523
524 callback.call(
525 &ChangingTitle {
526 first_title: first,
527 remaining_title: remaining,
528 content,
529 gap: 5.,
530 collapse: true,
531 }
532 .debug(0),
533 );
534 });
535 assert_binary_snapshot!(".pdf", bytes);
536 }
537
538 #[test]
539 fn test_titled() {
540 let bytes = test_element_bytes(
541 TestElementParams {
542 first_height: 27.,
543 width: WidthConstraint {
544 max: TestElementParams::DEFAULT_MAX_WIDTH,
545 expand: false,
546 },
547 ..TestElementParams::breakable()
548 },
549 |mut callback| {
550 let font = BuiltinFont::courier(callback.pdf());
551
552 let title = Text::basic("title", &font, 12.);
553 let title = title.debug(1);
554
555 let first = Text::basic("first", &font, 12.);
556 let first = first.debug(3);
557
558 let remaining = Text::basic("remaining\nremaining", &font, 12.);
559 let remaining = remaining.debug(4);
560
561 let content = Text::basic(LOREM_IPSUM, &font, 32.);
562 let content = content.debug(5);
563
564 let changing_title = ChangingTitle {
565 first_title: first,
566 remaining_title: remaining,
567 content,
568 gap: 5.,
569 collapse: true,
570 };
571 let changing_title = changing_title.debug(2);
572
573 callback.call(
574 &Titled {
575 title,
576 content: changing_title,
577 gap: 2.,
578 collapse_on_empty_content: true,
579 }
580 .debug(0)
581 .show_max_width()
582 .show_last_location_max_height(),
583 );
584 },
585 );
586 assert_binary_snapshot!(".pdf", bytes);
587 }
588}