1use crate::*;
2
3pub struct MinFirstHeight<E: Element> {
4 pub element: E,
5 pub min_first_height: f32,
6}
7
8impl<E: Element> Element for MinFirstHeight<E> {
9 fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
10 use FirstLocationUsage::*;
11
12 let layout = self.layout(
13 ctx.text_pieces_cache,
14 ctx.width,
15 ctx.first_height,
16 ctx.full_height,
17 );
18
19 if layout.pre_break {
20 match self.element.first_location_usage(FirstLocationUsageCtx {
21 text_pieces_cache: ctx.text_pieces_cache,
22 width: ctx.width,
23 first_height: ctx.full_height,
24 full_height: ctx.full_height,
25 }) {
26 NoneHeight => NoneHeight, _ => WillSkip,
28 }
29 } else {
30 match layout.measured {
31 Some(measure_output) if measure_output.break_count == 0 => {
32 if measure_output.size.height.is_none() {
33 NoneHeight
34 } else {
35 WillUse
36 }
37 }
38 _ => self.element.first_location_usage(ctx),
39 }
40 }
41 }
42
43 fn measure(&self, ctx: MeasureCtx) -> ElementSize {
44 if let Some(breakable) = ctx.breakable {
45 let location_offset;
46 let first_height;
47
48 let layout = self.layout(
49 ctx.text_pieces_cache,
50 ctx.width,
51 ctx.first_height,
52 breakable.full_height,
53 );
54
55 if layout.pre_break {
56 first_height = breakable.full_height;
57 location_offset = 1;
58 } else {
59 first_height = ctx.first_height;
60 location_offset = 0;
61 }
62
63 let size = if let Some(measure_output) = layout.measured {
64 *breakable.break_count = measure_output.break_count;
65 *breakable.extra_location_min_height = measure_output.extra_location_min_height;
66 measure_output.size
67 } else {
68 self.element.measure(MeasureCtx {
69 text_pieces_cache: ctx.text_pieces_cache,
70 width: ctx.width,
71 first_height,
72 breakable: Some(BreakableMeasure {
73 full_height: breakable.full_height,
74 break_count: breakable.break_count,
75 extra_location_min_height: breakable.extra_location_min_height,
76 }),
77 })
78 };
79
80 *breakable.break_count += location_offset;
81 size
82 } else {
83 self.element.measure(ctx)
84 }
85 }
86
87 fn draw(&self, ctx: DrawCtx) -> ElementSize {
88 if let Some(breakable) = ctx.breakable {
89 let location;
90 let first_height;
91 let preferred_height;
92 let location_offset;
93
94 if self
95 .layout(
96 ctx.text_pieces_cache,
97 ctx.width,
98 ctx.first_height,
99 breakable.full_height,
100 )
101 .pre_break
102 {
103 location = (breakable.do_break)(ctx.pdf, 0, None);
104 location_offset = 1;
105 first_height = breakable.full_height;
106 preferred_height = if breakable.preferred_height_break_count == 0 {
107 None
108 } else {
109 ctx.preferred_height
110 };
111 } else {
112 location = ctx.location;
113 location_offset = 0;
114 first_height = ctx.first_height;
115 preferred_height = ctx.preferred_height;
116 }
117
118 self.element.draw(DrawCtx {
119 pdf: ctx.pdf,
120 text_pieces_cache: ctx.text_pieces_cache,
121 location,
122 width: ctx.width,
123 first_height,
124 preferred_height,
125 breakable: Some(BreakableDraw {
126 full_height: breakable.full_height,
127 preferred_height_break_count: breakable
128 .preferred_height_break_count
129 .saturating_sub(location_offset),
130
131 do_break: &mut |pdf, location_idx, height| {
132 (breakable.do_break)(pdf, location_idx + location_offset, height)
133 },
134 }),
135 })
136 } else {
137 self.element.draw(ctx)
138 }
139 }
140}
141
142struct MeasureOutput {
143 size: ElementSize,
144 break_count: u32,
145 extra_location_min_height: Option<f32>,
146}
147
148struct Layout {
149 pre_break: bool,
150 measured: Option<MeasureOutput>,
151}
152
153impl<E: Element> MinFirstHeight<E> {
154 #[inline(always)]
155 fn layout(
156 &self,
157 text_pieces_cache: &TextPiecesCache,
158 width: WidthConstraint,
159 first_height: f32,
160 full_height: f32,
161 ) -> Layout {
162 let mut measured = None;
163 let pre_break = first_height < full_height && first_height < self.min_first_height && {
164 let mut break_count = 0;
165 let mut extra_location_min_height = None;
166
167 let size = self.element.measure(MeasureCtx {
168 text_pieces_cache,
169 width,
170 first_height,
171 breakable: Some(BreakableMeasure {
172 full_height,
173 break_count: &mut break_count,
174 extra_location_min_height: &mut extra_location_min_height,
175 }),
176 });
177
178 if break_count > 0 {
179 true
180 } else {
181 measured = Some(MeasureOutput {
182 size,
183 break_count,
184 extra_location_min_height,
185 });
186 false
187 }
188 };
189
190 Layout {
191 pre_break,
192 measured,
193 }
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use crate::{
201 elements::{none::NoneElement, ref_element::RefElement, text::Text},
202 fonts::builtin::BuiltinFont,
203 test_utils::{record_passes::RecordPasses, *},
204 };
205 use insta::assert_debug_snapshot;
206
207 #[test]
208 fn test_unbreakable() {
209 let output = test_element(
210 TestElementParams {
211 width: WidthConstraint {
212 max: 12.,
213 expand: false,
214 },
215 first_height: 12.,
216 breakable: None,
217 pos: (7., 20.0),
218 ..Default::default()
219 },
220 |assert, callback| {
221 let content = RecordPasses::new(FakeText {
222 lines: 1,
223 line_height: 5.,
224 width: 3.,
225 });
226
227 let element = MinFirstHeight {
228 element: RefElement(&content),
229 min_first_height: 10.,
230 };
231
232 let ret = callback.call(element);
233
234 if assert {
235 assert_debug_snapshot!(content.into_passes());
236 }
237
238 ret
239 },
240 );
241
242 assert_debug_snapshot!(output);
243 }
244
245 #[test]
246 fn test_breakable() {
247 let output = test_element(
248 TestElementParams {
249 width: WidthConstraint {
250 max: 12.,
251 expand: false,
252 },
253 first_height: 12.,
254 breakable: Some(TestElementParamsBreakable {
255 preferred_height_break_count: 0,
256 full_height: 15.,
257 }),
258 pos: (7., 20.0),
259 ..Default::default()
260 },
261 |assert, callback| {
262 let content = RecordPasses::new(FakeText {
263 lines: 1,
264 line_height: 5.,
265 width: 3.,
266 });
267
268 let element = MinFirstHeight {
269 element: RefElement(&content),
270 min_first_height: 10.,
271 };
272
273 let ret = callback.call(element);
274
275 if assert {
276 assert_debug_snapshot!(content.into_passes());
277 }
278
279 ret
280 },
281 );
282
283 assert_debug_snapshot!(output);
284 }
285
286 #[test]
287 fn test_pre_break() {
288 use crate::test_utils::binary_snapshots::*;
289 use insta::*;
290
291 let bytes = test_element_bytes(
292 TestElementParams {
293 first_height: 9.,
294 ..TestElementParams::breakable()
295 },
296 |mut callback| {
297 let font = BuiltinFont::courier(callback.pdf());
298
299 let content = Text::basic(LOREM_IPSUM, &font, 12.);
300 let content = content.debug(1).show_max_width();
301
302 callback.call(
303 &MinFirstHeight {
304 element: content,
305 min_first_height: 10.,
306 }
307 .debug(0)
308 .show_max_width()
309 .show_last_location_max_height(),
310 );
311 },
312 );
313 assert_binary_snapshot!(".pdf", bytes);
314 }
315
316 #[test]
317 fn test_collapse() {
318 let output = test_element(
319 TestElementParams {
320 width: WidthConstraint {
321 max: 12.,
322 expand: false,
323 },
324 first_height: 9.,
325 breakable: Some(TestElementParamsBreakable {
326 preferred_height_break_count: 3,
327 full_height: 15.,
328 }),
329 pos: (7., 20.0),
330 preferred_height: Some(4.),
331 ..Default::default()
332 },
333 |assert, callback| {
334 let content = RecordPasses::new(NoneElement);
335
336 let element = MinFirstHeight {
337 element: RefElement(&content),
338 min_first_height: 10.,
339 };
340
341 let ret = callback.call(element);
342
343 if assert {
344 assert_debug_snapshot!(content.into_passes());
345 }
346
347 ret
348 },
349 );
350
351 assert_debug_snapshot!(output);
352 }
353}