1use crate::{
2 utils::{mm_to_pt, u32_to_color_and_alpha},
3 *,
4};
5
6pub struct StyledBox<E: Element> {
7 pub element: E,
8 pub padding_left: f32,
9 pub padding_right: f32,
10 pub padding_top: f32,
11 pub padding_bottom: f32,
12 pub border_radius: f32,
13 pub fill: Option<u32>,
14 pub outline: Option<LineStyle>,
15}
16
17impl<E: Element> StyledBox<E> {
18 pub fn new(element: E) -> Self {
19 StyledBox {
20 element,
21 padding_top: 0.,
22 padding_bottom: 0.,
23 padding_left: 0.,
24 padding_right: 0.,
25 border_radius: 0.,
26 fill: None,
27 outline: None,
28 }
29 }
30}
31
32struct Common {
33 top: f32,
34 bottom: f32,
35 left: f32,
36 right: f32,
37
38 inner_width_constraint: WidthConstraint,
39 width: Option<f32>,
40}
41
42impl Common {
43 fn location(&self, pdf: &mut Pdf, location: &Location) -> Location {
44 Location {
45 pos: (location.pos.0 + self.left, location.pos.1 - self.top),
46 ..location.next_layer(pdf)
47 }
48 }
49
50 fn height(&self, input: f32) -> f32 {
51 input - self.top - self.bottom
52 }
53}
54
55impl<E: Element> StyledBox<E> {
56 fn common(&self, width: WidthConstraint) -> Common {
57 let extra_outline_offset = self.outline.map(|o| o.thickness).unwrap_or(0.0);
58
59 let top = self.padding_top + extra_outline_offset;
60 let bottom = self.padding_bottom + extra_outline_offset;
61 let left = self.padding_left + extra_outline_offset;
62 let right = self.padding_right + extra_outline_offset;
63
64 let inner_width_constraint = WidthConstraint {
65 max: width.max - left - right,
66 expand: width.expand,
67 };
68
69 let width = width.expand.then_some(inner_width_constraint.max);
70
71 Common {
72 top,
73 bottom,
74 left,
75 right,
76 inner_width_constraint,
77 width,
78 }
79 }
80
81 fn size(&self, common: &Common, size: ElementSize) -> ElementSize {
82 ElementSize {
83 width: common
84 .width
85 .or(size.width)
86 .map(|w| w + common.left + common.right),
87 height: size.height.map(|h| h + common.top + common.bottom),
88 }
89 }
90
91 fn draw_box(&self, pdf: &mut Pdf, location: &Location, size: (f32, f32)) {
92 use kurbo::{PathEl, RoundedRect, Shape};
93
94 let size = (
95 size.0 + self.padding_left + self.padding_right,
96 size.1 + self.padding_top + self.padding_bottom,
97 );
98
99 let thickness = self.outline.map(|o| o.thickness).unwrap_or(0.);
100 let half_thickness = thickness / 2.;
101
102 let fill_alpha = self
103 .fill
104 .map(|c| u32_to_color_and_alpha(c).1)
105 .filter(|&a| a != 1.);
106
107 let outline_alpha = self
108 .outline
109 .map(|o| u32_to_color_and_alpha(o.color).1)
110 .filter(|&a| a != 1.);
111
112 location.layer(pdf).save_state();
113
114 if fill_alpha.is_some() || outline_alpha.is_some() {
115 let ext_graphics_ref = pdf.alloc();
116
117 let mut ext_graphics = pdf.pdf.ext_graphics(ext_graphics_ref);
118 fill_alpha.inspect(|&a| {
119 ext_graphics.non_stroking_alpha(a);
120 });
121 outline_alpha.inspect(|&a| {
122 ext_graphics.stroking_alpha(a);
123 });
124
125 let resource_id = pdf.pages[location.page_idx].add_ext_g_state(ext_graphics_ref);
126 drop(ext_graphics);
127 location
128 .layer(pdf)
129 .set_parameters(Name(format!("{}", resource_id).as_bytes()));
130 }
131
132 let layer = location.layer(pdf);
133
134 if let Some(color) = self.fill {
135 let (color, _) = u32_to_color_and_alpha(color);
136
137 layer.set_fill_rgb(color[0], color[1], color[2]);
138 }
139
140 if let Some(line_style) = self.outline {
141 let (color, _) = u32_to_color_and_alpha(line_style.color);
142
143 layer
144 .set_line_width(mm_to_pt(thickness as f32))
145 .set_stroke_rgb(color[0], color[1], color[2])
146 .set_line_cap(line_style.cap_style.into());
147
148 if let Some(pattern) = line_style.dash_pattern {
149 layer.set_dash_pattern(pattern.dashes.map(f32::from), pattern.offset as f32);
150 }
151 }
152
153 let shape = RoundedRect::new(
154 mm_to_pt(location.pos.0 + half_thickness) as f64,
155 mm_to_pt(location.pos.1 - half_thickness) as f64,
156 mm_to_pt(location.pos.0 + size.0 + thickness + half_thickness) as f64,
157 mm_to_pt(location.pos.1 - size.1 - thickness - half_thickness) as f64,
158 mm_to_pt(self.border_radius) as f64,
159 );
160
161 let els = shape.path_elements(0.1);
162
163 let mut closed = false;
164
165 for el in els {
166 use PathEl::*;
167
168 match el {
169 MoveTo(point) => {
170 layer.move_to(point.x as f32, point.y as f32);
171 }
172 LineTo(point) => {
173 layer.line_to(point.x as f32, point.y as f32);
174 }
175 QuadTo(a, b) => {
176 layer.cubic_to_initial(a.x as f32, a.y as f32, b.x as f32, b.y as f32);
177 }
178 CurveTo(a, b, c) => {
179 layer.cubic_to(
180 a.x as f32, a.y as f32, b.x as f32, b.y as f32, c.x as f32, c.y as f32,
181 );
182 }
183 ClosePath => closed = true,
184 };
185 }
186
187 match (self.outline.is_some(), self.fill.is_some(), closed) {
188 (true, true, true) => layer.close_fill_nonzero_and_stroke(),
189 (true, true, false) => layer.fill_nonzero(),
190 (true, false, true) => layer.close_and_stroke(),
191 (true, false, false) => layer.stroke(),
192 (false, true, _) => layer.fill_nonzero(),
193 _ => layer.end_path(),
194 };
195
196 layer.restore_state();
197 }
198}
199
200impl<E: Element> Element for StyledBox<E> {
201 fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
202 let common = self.common(ctx.width);
203 let first_height = common.height(ctx.first_height);
204 let full_height = common.height(ctx.full_height);
205
206 self.element.first_location_usage(FirstLocationUsageCtx {
207 text_pieces_cache: ctx.text_pieces_cache,
208 width: common.inner_width_constraint,
209 first_height,
210 full_height,
211 })
212 }
213
214 fn measure(&self, ctx: MeasureCtx) -> ElementSize {
215 let common = self.common(ctx.width);
216 let first_height = common.height(ctx.first_height);
217
218 let size = if let Some(breakable) = ctx.breakable {
219 let full_height = common.height(breakable.full_height);
220
221 let size = self.element.measure(MeasureCtx {
222 text_pieces_cache: ctx.text_pieces_cache,
223 width: common.inner_width_constraint,
224 first_height,
225 breakable: Some(BreakableMeasure {
226 full_height,
227 break_count: breakable.break_count,
228 extra_location_min_height: breakable.extra_location_min_height,
229 }),
230 });
231
232 *breakable.extra_location_min_height = breakable
233 .extra_location_min_height
234 .map(|x| x + self.padding_top + self.padding_bottom);
235
236 size
237 } else {
238 self.element.measure(MeasureCtx {
239 text_pieces_cache: ctx.text_pieces_cache,
240 width: common.inner_width_constraint,
241 first_height,
242 breakable: None,
243 })
244 };
245
246 self.size(&common, size)
247 }
248
249 fn draw(&self, ctx: DrawCtx) -> ElementSize {
250 let common = self.common(ctx.width);
251 let first_height = common.height(ctx.first_height);
252
253 let size = if let Some(breakable) = ctx.breakable {
254 let full_height = common.height(breakable.full_height);
255
256 let mut break_count = 0;
257
258 let width = if ctx.width.expand {
259 Some(ctx.width.max - common.left - common.right)
260 } else {
261 let mut break_count = 0;
262 let mut extra_location_min_height = None;
263
264 self.element
265 .measure(MeasureCtx {
266 text_pieces_cache: ctx.text_pieces_cache,
267 width: common.inner_width_constraint,
268 first_height,
269 breakable: Some(BreakableMeasure {
270 full_height,
271 break_count: &mut break_count,
272 extra_location_min_height: &mut extra_location_min_height,
273 }),
274 })
275 .width
276 };
277
278 let element_location = common.location(ctx.pdf, &ctx.location);
279 let mut last_location = ctx.location;
280 let size = self.element.draw(DrawCtx {
281 pdf: ctx.pdf,
282 text_pieces_cache: ctx.text_pieces_cache,
283 location: element_location,
284 width: common.inner_width_constraint,
285 first_height,
286 preferred_height: ctx.preferred_height.map(|p| common.height(p)),
287 breakable: Some(BreakableDraw {
288 full_height,
289 preferred_height_break_count: breakable.preferred_height_break_count,
290 do_break: &mut |pdf, location_idx, height| {
291 let location = (breakable.do_break)(
292 pdf,
293 location_idx,
294 height.map(|h| h + common.top + common.bottom),
295 );
296
297 match (width, height) {
298 (Some(width), Some(height)) if location_idx >= break_count => {
299 let location = if location_idx == break_count {
300 &last_location
301 } else {
302 &(breakable.do_break)(pdf, location_idx, None)
303 };
304
305 self.draw_box(pdf, location, (width, height));
306 }
307 _ => (),
308 }
309
310 let ret = common.location(pdf, &location);
311 if location_idx >= break_count {
312 last_location = location;
313 }
314 break_count = break_count.max(location_idx + 1);
315 ret
316 },
317 }),
318 });
319
320 if let (Some(width), Some(height)) = (width, size.height) {
321 self.draw_box(ctx.pdf, &last_location, (width, height));
322 }
323
324 size
325 } else {
326 let location = common.location(ctx.pdf, &ctx.location);
327
328 let size = self.element.draw(DrawCtx {
329 pdf: ctx.pdf,
330 text_pieces_cache: ctx.text_pieces_cache,
331 location,
332 preferred_height: ctx.preferred_height.map(|p| common.height(p)),
333 width: common.inner_width_constraint,
334 first_height,
335 breakable: None,
336 });
337
338 if let ElementSize {
339 width: Some(width),
340 height: Some(height),
341 } = size
342 {
343 self.draw_box(ctx.pdf, &ctx.location, (width, height));
344 }
345
346 size
347 };
348
349 self.size(&common, size)
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356 use crate::{
357 elements::{
358 rectangle::Rectangle,
359 ref_element::RefElement,
360 row::{Flex, Row},
361 text::Text,
362 },
363 fonts::builtin::BuiltinFont,
364 test_utils::{
365 record_passes::{Break, BreakableDraw, DrawPass, RecordPasses},
366 *,
367 },
368 };
369
370 #[test]
371 fn test_unbreakable() {
372 let width = WidthConstraint {
373 max: 7.,
374 expand: false,
375 };
376 let first_height = 30.;
377 let pos = (2., 10.);
378
379 let output = test_element(
380 TestElementParams {
381 width,
382 first_height,
383 breakable: None,
384 pos,
385 ..Default::default()
386 },
387 |assert, callback| {
388 let content = RecordPasses::new(FakeText {
389 lines: 3,
390 line_height: 5.,
391 width: 3.,
392 });
393
394 let element = StyledBox {
395 padding_left: 1.,
396 padding_right: 2.,
397 padding_top: 3.,
398 padding_bottom: 4.,
399
400 ..StyledBox::new(RefElement(&content))
401 };
402
403 let ret = callback.call(element);
404
405 if assert {
406 content.assert_first_location_usage_count(0);
407 content.assert_measure_count(0);
408 content.assert_draw(DrawPass {
409 width: WidthConstraint {
410 max: 4.,
411 expand: false,
412 },
413 first_height: 23.,
414 preferred_height: None,
415 page: 0,
416 layer: 1,
417 pos: (3., 7.),
418 breakable: None,
419 });
420 }
421
422 ret
423 },
424 );
425
426 output.assert_size(ElementSize {
427 width: Some(6.),
428 height: Some(22.),
429 });
430 }
431
432 #[test]
433 fn test_pre_break() {
434 let width = WidthConstraint {
435 max: 7.,
436 expand: false,
437 };
438 let first_height = 9.;
439 let full_height = 18.;
440 let pos = (2., 18.);
441
442 let output = test_element(
443 TestElementParams {
444 width,
445 first_height,
446 breakable: Some(TestElementParamsBreakable {
447 full_height,
448 ..Default::default()
449 }),
450 pos,
451 ..Default::default()
452 },
453 |assert, callback| {
454 let content = RecordPasses::new(FakeText {
455 lines: 3,
456 line_height: 5.,
457 width: 3.,
458 });
459
460 let element = StyledBox {
461 padding_left: 1.,
462 padding_right: 2.,
463 padding_top: 3.,
464 padding_bottom: 4.,
465
466 ..StyledBox::new(RefElement(&content))
467 };
468
469 let ret = callback.call(element);
470
471 if assert {
472 content.assert_first_location_usage_count(0);
473 content.assert_measure_count(1);
474 content.assert_draw(DrawPass {
475 width: WidthConstraint {
476 max: 4.,
477 expand: false,
478 },
479 first_height: 2.,
480 preferred_height: None,
481 page: 0,
482 layer: 1,
483 pos: (3., 6.),
484 breakable: Some(BreakableDraw {
485 full_height: 11.,
486 preferred_height_break_count: 0,
487 breaks: vec![
488 Break {
490 page: 1,
491 layer: 1,
492 pos: (3., 15.),
493 },
494 Break {
495 page: 2,
496 layer: 1,
497 pos: (3., 15.),
498 },
499 ],
500 }),
501 });
502 }
503
504 ret
505 },
506 );
507
508 output.assert_size(ElementSize {
509 width: Some(6.),
510 height: Some(12.),
511 });
512
513 output
514 .breakable
515 .unwrap()
516 .assert_first_location_usage(FirstLocationUsage::WillSkip)
517 .assert_break_count(2)
518 .assert_extra_location_min_height(None);
519 }
520
521 #[test]
522 fn test_x_size() {
523 use crate::test_utils::binary_snapshots::*;
524 use insta::*;
525
526 let bytes = test_element_bytes(TestElementParams::breakable(), |callback| {
527 let first = Rectangle {
528 size: (12., 12.),
529 fill: Some(0x00_00_77_FF),
530 outline: Some((2., 0x00_00_00_FF)),
531 };
532 let first = first.debug(1).show_max_width();
533
534 callback.call(
535 &StyledBox {
536 element: first,
537 padding_left: 1.,
538 padding_right: 2.,
539 padding_top: 3.,
540 padding_bottom: 4.,
541 border_radius: 1.,
542 fill: None,
543 outline: Some(LineStyle {
544 thickness: 1.,
545 color: 0x00_00_00_FF,
546 dash_pattern: None,
547 cap_style: LineCapStyle::Butt,
548 }),
549 }
550 .debug(0)
551 .show_max_width()
552 .show_last_location_max_height(),
553 );
554 });
555 assert_binary_snapshot!(".pdf", bytes);
556 }
557
558 #[test]
559 fn test_border_sizing() {
560 use crate::test_utils::binary_snapshots::*;
561 use insta::*;
562
563 let bytes = test_element_bytes(TestElementParams::breakable(), |callback| {
564 let first = Rectangle {
565 size: (12., 12.),
566 fill: Some(0x00_00_77_FF),
567 outline: None,
568 };
569 let first = first.debug(1).show_max_width();
570
571 callback.call(
572 &StyledBox {
573 outline: Some(LineStyle {
574 thickness: 32.,
575 color: 0x00_00_00_FF,
576 dash_pattern: None,
577 cap_style: LineCapStyle::Butt,
578 }),
579 ..StyledBox::new(first)
580 }
581 .debug(0)
582 .show_max_width()
583 .show_last_location_max_height(),
584 );
585 });
586 assert_binary_snapshot!(".pdf", bytes);
587 }
588
589 #[test]
590 fn test_last_location() {
591 use crate::test_utils::binary_snapshots::*;
592 use insta::*;
593
594 let bytes = test_element_bytes(TestElementParams::breakable(), |mut callback| {
595 let font = BuiltinFont::courier(callback.pdf());
596
597 let text_a = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam mauris \
598 massa, sollicitudin nec nunc eu, lacinia venenatis felis. Etiam non tempus nisl, \
599 euismod accumsan arcu. Vivamus aliquam lorem a odio maximus volutpat. Phasellus \
600 volutpat leo quis varius posuere. Nam sagittis nisl eget suscipit pretium. Donec \
601 varius tortor eget nibh maximus sagittis. Duis id libero eu mi vulputate congue id \
602 eu est. Maecenas pellentesque massa id dui fringilla, et porta nulla imperdiet. \
603 Sed eu est rutrum, scelerisque tortor ac, lacinia turpis. \
604 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam mauris \
605 massa, sollicitudin nec nunc eu, lacinia venenatis felis. Etiam non tempus nisl, \
606 euismod accumsan arcu. Vivamus aliquam lorem a odio maximus volutpat. Phasellus \
607 volutpat leo quis varius posuere. Nam sagittis nisl eget suscipit pretium. Donec \
608 varius tortor eget nibh maximus sagittis. Duis id libero eu mi vulputate congue id \
609 eu est. Maecenas pellentesque massa id dui fringilla, et porta nulla imperdiet. \
610 Sed eu est rutrum, scelerisque tortor ac, lacinia turpis. \
611 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam mauris \
612 massa, sollicitudin nec nunc eu, lacinia venenatis felis. Etiam non tempus nisl, \
613 euismod accumsan arcu. Vivamus aliquam lorem a odio maximus volutpat. Phasellus \
614 volutpat leo quis varius posuere. Nam sagittis nisl eget suscipit pretium. Donec \
615 varius tortor eget nibh maximus sagittis. Duis id libero eu mi vulputate congue id \
616 eu est. Maecenas pellentesque massa id dui fringilla, et porta nulla imperdiet. \
617 Sed eu est rutrum, scelerisque tortor ac, lacinia turpis.";
618 let text_b = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam mauris \
619 massa, sollicitudin nec nunc eu, lacinia venenatis felis. Etiam non tempus nisl, \
620 euismod accumsan arcu. Vivamus aliquam lorem a odio maximus volutpat. Phasellus \
621 volutpat leo quis varius posuere. Nam sagittis nisl eget suscipit pretium. Donec \
622 varius tortor eget nibh maximus sagittis. Duis id libero eu mi vulputate congue id \
623 eu est. Maecenas pellentesque massa id dui fringilla, et porta nulla imperdiet. \
624 Sed eu est rutrum, scelerisque tortor ac, lacinia turpis. \
625 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam mauris \
626 massa, sollicitudin nec nunc eu, lacinia venenatis felis. Etiam non tempus nisl, \
627 euismod accumsan arcu. Vivamus aliquam lorem a odio maximus volutpat. Phasellus \
628 volutpat leo quis varius posuere. Nam sagittis nisl eget suscipit pretium. Donec \
629 varius tortor eget nibh maximus sagittis. Duis id libero eu mi vulputate congue id \
630 eu est. Maecenas pellentesque massa id dui fringilla, et porta nulla imperdiet. \
631 Sed eu est rutrum, scelerisque tortor ac, lacinia turpis.";
632
633 callback.call(
634 &StyledBox {
635 fill: Some(0x00_00_FF_FF),
636 ..StyledBox::new(Row::new(|content| {
637 content.add(&Text::new(text_a, &font, 24.), Flex::Expand(1));
638 content.add(&Text::new(text_b, &font, 24.), Flex::Expand(1));
639 }))
640 }
641 .debug(0)
642 .show_max_width()
643 .show_last_location_max_height(),
644 );
645 });
646 assert_binary_snapshot!(".pdf", bytes);
647 }
648}