1use crate::*;
2
3pub struct AlignPreferredHeightBottom<E: Element>(pub E);
4
5impl<E: Element> Element for AlignPreferredHeightBottom<E> {
6 fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
7 let layout = self.layout(
8 ctx.text_pieces_cache,
9 ctx.width,
10 ctx.first_height,
11 Some(ctx.full_height),
12 0,
13 0.,
14 );
15
16 if layout.breaks > 0 {
17 FirstLocationUsage::WillSkip
18 } else if layout.size.height.is_some() {
19 FirstLocationUsage::WillUse
20 } else {
21 FirstLocationUsage::NoneHeight
22 }
23 }
24
25 fn measure(&self, mut ctx: MeasureCtx) -> ElementSize {
26 let layout = self.layout(
27 ctx.text_pieces_cache,
28 ctx.width,
29 ctx.first_height,
30 ctx.breakable.as_ref().map(|b| b.full_height),
31 0,
32 0.,
33 );
34
35 if layout.breaks > 0 {
36 let breakable = ctx.breakable.as_mut().unwrap();
37
38 *breakable.break_count = layout.breaks;
39
40 Some(breakable.full_height)
41 } else {
42 layout.size.height.map(|_| ctx.first_height)
43 };
44
45 if let Some(breakable) = ctx.breakable {
46 *breakable.extra_location_min_height = layout.size.height;
47 }
48
49 ElementSize {
50 width: layout.size.width,
51 height: layout.size.height.map(|h| h + layout.y_offset),
52 }
53 }
54
55 fn draw(&self, ctx: DrawCtx) -> ElementSize {
56 let layout = self.layout(
57 ctx.text_pieces_cache,
58 ctx.width,
59 ctx.first_height,
60 ctx.breakable.as_ref().map(|b| b.full_height),
61 ctx.breakable
62 .as_ref()
63 .map(|b| b.preferred_height_break_count)
64 .unwrap_or(0),
65 ctx.preferred_height.unwrap_or(0.),
66 );
67
68 let height_available;
69 let mut location;
70
71 if layout.breaks > 0 {
72 let breakable = ctx.breakable.unwrap();
73
74 location = (breakable.do_break)(ctx.pdf, layout.breaks - 1, None);
75 height_available = breakable.full_height;
76 } else {
77 location = ctx.location;
78 height_available = ctx.first_height;
79 }
80
81 location.pos.1 -= layout.y_offset;
82
83 self.0.draw(DrawCtx {
84 pdf: ctx.pdf,
85 text_pieces_cache: ctx.text_pieces_cache,
86 location,
87 width: ctx.width,
88 first_height: height_available,
89 preferred_height: None,
90 breakable: None,
91 });
92
93 ElementSize {
94 width: layout.size.width,
95 height: layout.size.height.map(|h| h + layout.y_offset),
96 }
97 }
98}
99
100#[derive(Debug)]
101struct Layout {
102 breaks: u32,
103 y_offset: f32,
104 size: ElementSize,
105}
106
107impl<E: Element> AlignPreferredHeightBottom<E> {
108 fn layout(
109 &self,
110 text_pieces_cache: &TextPiecesCache,
111 width: WidthConstraint,
112 first_height: f32,
113 full_height: Option<f32>,
114 preferred_breaks: u32,
115 preferred_height: f32,
116 ) -> Layout {
117 let height_available = full_height.unwrap_or(first_height);
118
119 let size = self.0.measure(MeasureCtx {
120 text_pieces_cache,
121 width,
122 first_height: height_available,
123 breakable: None,
124 });
125
126 let breaks;
127 let location_height;
128
129 if let (Some(height), Some(_)) = (size.height, full_height) {
130 if preferred_breaks == 0 && height > first_height {
131 breaks = 1;
132 location_height = 0.;
133 } else {
134 breaks = preferred_breaks;
135 location_height = preferred_height;
136 }
137 } else {
138 breaks = 0;
139 location_height = preferred_height;
140 };
141
142 let y_offset = if let Some(height) = size.height {
143 (location_height - height).max(0.)
144 } else {
145 0.
146 };
147
148 Layout {
149 breaks,
150 y_offset,
151 size,
152 }
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::{
160 elements::ref_element::RefElement,
161 test_utils::{record_passes::RecordPasses, *},
162 };
163 use insta::*;
164
165 #[test]
166 fn test_unbreakable() {
167 let output = test_element(
168 TestElementParams {
169 width: WidthConstraint {
170 max: 12.,
171 expand: true,
172 },
173 first_height: 21.,
174 breakable: None,
175 pos: (11., 29.0),
176 ..Default::default()
177 },
178 |assert, callback| {
179 let content = RecordPasses::new(FakeText {
180 lines: 3,
181 line_height: 5.,
182 width: 3.,
183 });
184
185 let element = AlignPreferredHeightBottom(RefElement(&content));
186
187 let ret = callback.call(element);
188
189 if assert {
190 assert_debug_snapshot!(content.into_passes());
191 }
192
193 ret
194 },
195 );
196
197 assert_debug_snapshot!(output);
198 }
199
200 #[test]
201 fn test_unbreakable_preferred_height() {
202 let output = test_element(
203 TestElementParams {
204 width: WidthConstraint {
205 max: 12.,
206 expand: true,
207 },
208 first_height: 21.,
209 breakable: None,
210 pos: (11., 29.0),
211 preferred_height: Some(17.),
212 ..Default::default()
213 },
214 |assert, callback| {
215 let content = RecordPasses::new(FakeText {
216 lines: 3,
217 line_height: 5.,
218 width: 3.,
219 });
220
221 let element = AlignPreferredHeightBottom(RefElement(&content));
222
223 let ret = callback.call(element);
224
225 if assert {
226 assert_debug_snapshot!(content.into_passes());
227 }
228
229 ret
230 },
231 );
232
233 assert_debug_snapshot!(output);
234 }
235
236 #[test]
237 fn test_breakable() {
238 let output = test_element(
239 TestElementParams {
240 width: WidthConstraint {
241 max: 12.,
242 expand: true,
243 },
244 first_height: 21.,
245 breakable: Some(TestElementParamsBreakable {
246 full_height: 25.,
247 ..Default::default()
248 }),
249 pos: (11., 29.0),
250 ..Default::default()
251 },
252 |assert, callback| {
253 let content = RecordPasses::new(FakeText {
254 lines: 3,
255 line_height: 5.,
256 width: 3.,
257 });
258
259 let element = AlignPreferredHeightBottom(RefElement(&content));
260
261 let ret = callback.call(element);
262
263 if assert {
264 assert_debug_snapshot!(content.into_passes());
265 }
266
267 ret
268 },
269 );
270
271 assert_debug_snapshot!(output);
272 }
273
274 #[test]
275 fn test_pre_break() {
276 let output = test_element(
277 TestElementParams {
278 width: WidthConstraint {
279 max: 12.,
280 expand: true,
281 },
282 first_height: 21.,
283 breakable: Some(TestElementParamsBreakable {
284 full_height: 26.,
285 ..Default::default()
286 }),
287 pos: (11., 29.0),
288 ..Default::default()
289 },
290 |assert, callback| {
291 let content = RecordPasses::new(FakeText {
292 lines: 5,
293 line_height: 5.,
294 width: 3.,
295 });
296
297 let element = AlignPreferredHeightBottom(RefElement(&content));
298
299 let ret = callback.call(element);
300
301 if assert {
302 assert_debug_snapshot!(content.into_passes());
303 }
304
305 ret
306 },
307 );
308
309 assert_debug_snapshot!(output);
310 }
311
312 #[test]
313 fn test_pre_break_preferred_height() {
314 let width = WidthConstraint {
315 max: 12.,
316 expand: true,
317 };
318 let first_height = 21.;
319 let full_height = 26.;
320 let pos = (11., 29.0);
321
322 let output = test_element(
323 TestElementParams {
324 width,
325 first_height,
326 breakable: Some(TestElementParamsBreakable {
327 full_height,
328 ..Default::default()
329 }),
330 pos,
331 preferred_height: Some(20.),
332 ..Default::default()
333 },
334 |assert, callback| {
335 let content = RecordPasses::new(FakeText {
336 lines: 5,
337 line_height: 5.,
338 width: 3.,
339 });
340
341 let element = AlignPreferredHeightBottom(RefElement(&content));
342
343 let ret = callback.call(element);
344
345 if assert {
346 assert_debug_snapshot!(content.into_passes());
347 }
348
349 ret
350 },
351 );
352
353 assert_debug_snapshot!(output);
354 }
355
356 #[test]
357 fn test_preferred_breaks() {
358 let output = test_element(
359 TestElementParams {
360 width: WidthConstraint {
361 max: 12.,
362 expand: true,
363 },
364 first_height: 21.,
365 breakable: Some(TestElementParamsBreakable {
366 full_height: 26.,
367 preferred_height_break_count: 4,
368 }),
369 pos: (11., 29.0),
370 preferred_height: None,
371 ..Default::default()
372 },
373 |assert, callback| {
374 let content = RecordPasses::new(FakeText {
375 lines: 5,
376 line_height: 5.,
377 width: 3.,
378 });
379
380 let element = AlignPreferredHeightBottom(RefElement(&content));
381
382 let ret = callback.call(element);
383
384 if assert {
385 assert_debug_snapshot!(content.into_passes());
386 }
387
388 ret
389 },
390 );
391
392 assert_debug_snapshot!(output);
393 }
394
395 #[test]
396 fn test_preferred_height() {
397 let output = test_element(
398 TestElementParams {
399 width: WidthConstraint {
400 max: 12.,
401 expand: true,
402 },
403 first_height: 21.,
404 breakable: Some(TestElementParamsBreakable {
405 full_height: 23.,
406 preferred_height_break_count: 3,
407 }),
408 pos: (11., 29.0),
409 preferred_height: Some(21.5),
410 ..Default::default()
411 },
412 |assert, callback| {
413 let content = RecordPasses::new(FakeText {
414 lines: 4,
415 line_height: 5.,
416 width: 3.,
417 });
418
419 let element = AlignPreferredHeightBottom(RefElement(&content));
420
421 let ret = callback.call(element);
422
423 if assert {
424 assert_debug_snapshot!(content.into_passes());
425 }
426
427 ret
428 },
429 );
430
431 assert_debug_snapshot!(output);
432 }
433}