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