1use crate::*;
2
3pub struct ShrinkToFit<E: Element> {
8 pub element: E,
9 pub min_height: f32,
10}
11
12struct Layout {
13 pre_break: bool,
14 scale_factor: f32,
15 size: ElementSize,
16 scaled_size: ElementSize,
17 height: f32,
18}
19
20impl<E: Element> ShrinkToFit<E> {
21 fn layout(
22 &self,
23 text_pieces_cache: &TextPiecesCache,
24 width: WidthConstraint,
25 first_height: f32,
26 full_height: Option<f32>,
27 ) -> Layout {
28 let pre_break;
29
30 let available_height = if first_height >= self.min_height {
31 pre_break = false;
32
33 first_height
34 } else {
35 pre_break = full_height.is_some();
36
37 full_height.unwrap_or(first_height).max(self.min_height)
40 };
41
42 let size = self.element.measure(MeasureCtx {
43 text_pieces_cache,
44 width,
45 first_height: available_height,
46 breakable: None,
47 });
48
49 let height = size
50 .height
51 .map(|h| {
52 if h <= available_height {
53 available_height
54 } else {
55 h
56 }
57 })
58 .unwrap_or(available_height);
59
60 let scale_factor = size
61 .height
62 .map(|h| {
63 if h <= available_height {
64 1.
65 } else {
66 available_height / h
67 }
68 })
69 .unwrap_or(1.);
70
71 let scaled_size = ElementSize {
72 width: size.width.map(|w| w * scale_factor),
73 height: size.height.map(|h| h * scale_factor),
74 };
75
76 Layout {
77 pre_break,
78 scale_factor,
79 height,
80 size,
81 scaled_size,
82 }
83 }
84}
85
86impl<E: Element> Element for ShrinkToFit<E> {
87 fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
88 let layout = self.layout(
89 ctx.text_pieces_cache,
90 ctx.width,
91 ctx.first_height,
92 Some(ctx.full_height),
93 );
94
95 if layout.pre_break {
96 FirstLocationUsage::WillSkip
97 } else if layout.size.height.is_some() {
98 FirstLocationUsage::WillUse
99 } else {
100 FirstLocationUsage::NoneHeight
101 }
102 }
103
104 fn measure(&self, ctx: MeasureCtx) -> ElementSize {
105 let layout = self.layout(
106 ctx.text_pieces_cache,
107 ctx.width,
108 ctx.first_height,
109 ctx.breakable.as_ref().map(|b| b.full_height),
110 );
111
112 if layout.pre_break {
113 *ctx.breakable.unwrap().break_count = 1;
114 }
115
116 layout.scaled_size
117 }
118
119 fn draw(&self, ctx: DrawCtx) -> ElementSize {
120 let layout = self.layout(
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 location;
128
129 if layout.pre_break {
130 let breakable = ctx.breakable.unwrap();
131
132 location = (breakable.do_break)(ctx.pdf, 0, None);
133 } else {
134 location = ctx.location;
135 }
136
137 location
138 .layer(ctx.pdf)
139 .save_state()
140 .transform(utils::scale(layout.scale_factor));
141
142 self.element.draw(DrawCtx {
143 pdf: ctx.pdf,
144 text_pieces_cache: ctx.text_pieces_cache,
145 location: Location {
146 pos: (
147 location.pos.0 / layout.scale_factor,
148 location.pos.1 / layout.scale_factor,
149 ),
150 scale_factor: location.scale_factor * layout.scale_factor,
151 ..location.clone()
152 },
153 width: ctx.width,
154 first_height: layout.height,
155 preferred_height: None,
156 breakable: None,
157 });
158
159 location.layer(ctx.pdf).restore_state();
160
161 layout.scaled_size
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use elements::{align_location_bottom::AlignLocationBottom, styled_box::StyledBox};
168 use insta::assert_binary_snapshot;
169
170 use super::*;
171 use crate::{
172 elements::text::Text, fonts::builtin::BuiltinFont, test_utils::binary_snapshots::*,
173 };
174
175 #[test]
176 fn test_basic() {
177 let bytes = test_element_bytes(
178 TestElementParams {
179 first_height: 10.,
180 ..TestElementParams::breakable()
181 },
182 |mut callback| {
183 let font = BuiltinFont::courier(callback.pdf());
184 let text = Text::basic("TEST", &font, 100.);
185 let text = text
186 .debug(1)
187 .show_max_width()
188 .show_last_location_max_height();
189
190 let shrink_to_fit = ShrinkToFit {
191 element: text,
192 min_height: 9.,
193 };
194 let shrink_to_fit = &shrink_to_fit
195 .debug(0)
196 .show_max_width()
197 .show_last_location_max_height();
198
199 callback.call(shrink_to_fit);
200 },
201 );
202 assert_binary_snapshot!(".pdf", bytes);
203 }
204
205 #[test]
206 fn test_unbreakable_negative_first_height() {
207 let bytes = test_element_bytes(
208 TestElementParams {
209 first_height: -10.,
210 ..TestElementParams::unbreakable()
211 },
212 |mut callback| {
213 let font = BuiltinFont::courier(callback.pdf());
214 let text = Text::basic("TEST", &font, 100.);
215 let text = text
216 .debug(1)
217 .show_max_width()
218 .show_last_location_max_height();
219
220 let shrink_to_fit = ShrinkToFit {
221 element: text,
222 min_height: 9.,
223 };
224 let shrink_to_fit = &shrink_to_fit
225 .debug(0)
226 .show_max_width()
227 .show_last_location_max_height();
228
229 callback.call(shrink_to_fit);
230 },
231 );
232 assert_binary_snapshot!(".pdf", bytes);
233 }
234
235 #[test]
236 fn test_pre_break() {
237 let bytes = test_element_bytes(
238 TestElementParams {
239 first_height: 5.,
240 ..TestElementParams::breakable()
241 },
242 |mut callback| {
243 let font = BuiltinFont::courier(callback.pdf());
244 let text = Text::basic("T E S T", &font, 1024.);
245 let text = text
246 .debug(1)
247 .show_max_width()
248 .show_last_location_max_height();
249
250 let shrink_to_fit = ShrinkToFit {
251 element: text,
252 min_height: 10.,
253 };
254 let shrink_to_fit = &shrink_to_fit
255 .debug(0)
256 .show_max_width()
257 .show_last_location_max_height();
258
259 callback.call(shrink_to_fit);
260 },
261 );
262 assert_binary_snapshot!(".pdf", bytes);
263 }
264
265 #[test]
266 fn test_align_location_bottom() {
267 let bytes = test_element_bytes(
268 TestElementParams {
269 first_height: 20.,
270 ..TestElementParams::breakable()
271 },
272 |mut callback| {
273 let font = BuiltinFont::courier(callback.pdf());
274 let text = Text::basic("Test", &font, 20.);
275 let text = text
276 .debug(1)
277 .show_max_width()
278 .show_last_location_max_height();
279
280 let bottom = AlignLocationBottom(text);
281 let bottom = bottom.debug(2);
282
283 let shrink_to_fit = ShrinkToFit {
284 element: bottom,
285 min_height: 10.,
286 };
287 let shrink_to_fit = &shrink_to_fit
288 .debug(0)
289 .show_max_width()
290 .show_last_location_max_height();
291
292 callback.call(shrink_to_fit);
293 },
294 );
295 assert_binary_snapshot!(".pdf", bytes);
296 }
297
298 #[test]
299 fn test_layers() {
300 let bytes = test_element_bytes(
301 TestElementParams {
302 first_height: 20.,
303 ..TestElementParams::breakable()
304 },
305 |mut callback| {
306 let font = BuiltinFont::courier(callback.pdf());
307 let text = Text::basic("Test", &font, 100.);
308 let text = text
309 .debug(1)
310 .show_max_width()
311 .show_last_location_max_height();
312
313 let wrapper = StyledBox {
314 outline: Some(LineStyle {
315 thickness: 12.,
316 color: 0x00_00_00_FF,
317 dash_pattern: None,
318 cap_style: LineCapStyle::Round,
319 }),
320 ..StyledBox::new(text)
321 };
322 let wrapper = wrapper.debug(2);
323
324 let shrink_to_fit = ShrinkToFit {
325 element: wrapper,
326 min_height: 10.,
327 };
328 let shrink_to_fit = &shrink_to_fit
329 .debug(0)
330 .show_max_width()
331 .show_last_location_max_height();
332
333 callback.call(shrink_to_fit);
334 },
335 );
336 assert_binary_snapshot!(".pdf", bytes);
337 }
338
339 #[test]
340 fn test_nested_layers() {
341 let bytes = test_element_bytes(
342 TestElementParams {
343 first_height: 30.,
344 ..TestElementParams::breakable()
345 },
346 |mut callback| {
347 let font = BuiltinFont::courier(callback.pdf());
348 let text = Text::basic("Test", &font, 100.);
349 let text = text
350 .debug(1)
351 .show_max_width()
352 .show_last_location_max_height();
353
354 let wrapper = StyledBox {
355 outline: Some(LineStyle {
356 thickness: 10.,
357 color: 0x00_00_00_FF,
358 dash_pattern: None,
359 cap_style: LineCapStyle::Round,
360 }),
361 ..StyledBox::new(text)
362 };
363 let wrapper = wrapper.debug(2);
364 let shrink_to_fit = ShrinkToFit {
365 element: wrapper,
366 min_height: 10.,
367 };
368
369 let wrapper_1 = StyledBox {
370 outline: Some(LineStyle {
371 thickness: 10.,
372 color: 0xAA_00_00_FF,
373 dash_pattern: None,
374 cap_style: LineCapStyle::Round,
375 }),
376 ..StyledBox::new(shrink_to_fit)
377 };
378 let wrapper_1 = wrapper_1.debug(3);
379
380 let shrink_to_fit_1 = ShrinkToFit {
381 element: wrapper_1,
382 min_height: 10.,
383 };
384 let shrink_to_fit = &shrink_to_fit_1
385 .debug(0)
386 .show_max_width()
387 .show_last_location_max_height();
388
389 callback.call(shrink_to_fit);
390 },
391 );
392 assert_binary_snapshot!(".pdf", bytes);
393 }
394}