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 {
531 size: (12., 12.),
532 fill: Some(0x00_00_77_FF),
533 outline: Some((2., 0x00_00_00_FF)),
534 };
535 let first = first.debug(1).show_max_width();
536
537 callback.call(
538 &StyledBox {
539 element: first,
540 padding_left: 1.,
541 padding_right: 2.,
542 padding_top: 3.,
543 padding_bottom: 4.,
544 border_radius: 1.,
545 fill: None,
546 outline: Some(LineStyle {
547 thickness: 1.,
548 color: 0x00_00_00_FF,
549 dash_pattern: None,
550 cap_style: LineCapStyle::Butt,
551 }),
552 }
553 .debug(0)
554 .show_max_width()
555 .show_last_location_max_height(),
556 );
557 });
558 assert_binary_snapshot!(".pdf", bytes);
559 }
560
561 #[test]
562 fn test_border_sizing() {
563 use crate::test_utils::binary_snapshots::*;
564 use insta::*;
565
566 let bytes = test_element_bytes(TestElementParams::breakable(), |callback| {
567 let first = Rectangle {
568 size: (12., 12.),
569 fill: Some(0x00_00_77_FF),
570 outline: None,
571 };
572 let first = first.debug(1).show_max_width();
573
574 callback.call(
575 &StyledBox {
576 outline: Some(LineStyle {
577 thickness: 32.,
578 color: 0x00_00_00_FF,
579 dash_pattern: None,
580 cap_style: LineCapStyle::Butt,
581 }),
582 ..StyledBox::new(first)
583 }
584 .debug(0)
585 .show_max_width()
586 .show_last_location_max_height(),
587 );
588 });
589 assert_binary_snapshot!(".pdf", bytes);
590 }
591
592 #[test]
593 fn test_last_location() {
594 use crate::test_utils::binary_snapshots::*;
595 use insta::*;
596
597 let bytes = test_element_bytes(TestElementParams::breakable(), |mut callback| {
598 let font = BuiltinFont::courier(callback.pdf());
599
600 let text_a = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam mauris \
601 massa, sollicitudin nec nunc eu, lacinia venenatis felis. Etiam non tempus nisl, \
602 euismod accumsan arcu. Vivamus aliquam lorem a odio maximus volutpat. Phasellus \
603 volutpat leo quis varius posuere. Nam sagittis nisl eget suscipit pretium. Donec \
604 varius tortor eget nibh maximus sagittis. Duis id libero eu mi vulputate congue id \
605 eu est. Maecenas pellentesque massa id dui fringilla, et porta nulla imperdiet. \
606 Sed eu est rutrum, scelerisque tortor ac, lacinia turpis. \
607 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam mauris \
608 massa, sollicitudin nec nunc eu, lacinia venenatis felis. Etiam non tempus nisl, \
609 euismod accumsan arcu. Vivamus aliquam lorem a odio maximus volutpat. Phasellus \
610 volutpat leo quis varius posuere. Nam sagittis nisl eget suscipit pretium. Donec \
611 varius tortor eget nibh maximus sagittis. Duis id libero eu mi vulputate congue id \
612 eu est. Maecenas pellentesque massa id dui fringilla, et porta nulla imperdiet. \
613 Sed eu est rutrum, scelerisque tortor ac, lacinia turpis. \
614 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam mauris \
615 massa, sollicitudin nec nunc eu, lacinia venenatis felis. Etiam non tempus nisl, \
616 euismod accumsan arcu. Vivamus aliquam lorem a odio maximus volutpat. Phasellus \
617 volutpat leo quis varius posuere. Nam sagittis nisl eget suscipit pretium. Donec \
618 varius tortor eget nibh maximus sagittis. Duis id libero eu mi vulputate congue id \
619 eu est. Maecenas pellentesque massa id dui fringilla, et porta nulla imperdiet. \
620 Sed eu est rutrum, scelerisque tortor ac, lacinia turpis.";
621 let text_b = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam mauris \
622 massa, sollicitudin nec nunc eu, lacinia venenatis felis. Etiam non tempus nisl, \
623 euismod accumsan arcu. Vivamus aliquam lorem a odio maximus volutpat. Phasellus \
624 volutpat leo quis varius posuere. Nam sagittis nisl eget suscipit pretium. Donec \
625 varius tortor eget nibh maximus sagittis. Duis id libero eu mi vulputate congue id \
626 eu est. Maecenas pellentesque massa id dui fringilla, et porta nulla imperdiet. \
627 Sed eu est rutrum, scelerisque tortor ac, lacinia turpis. \
628 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam mauris \
629 massa, sollicitudin nec nunc eu, lacinia venenatis felis. Etiam non tempus nisl, \
630 euismod accumsan arcu. Vivamus aliquam lorem a odio maximus volutpat. Phasellus \
631 volutpat leo quis varius posuere. Nam sagittis nisl eget suscipit pretium. Donec \
632 varius tortor eget nibh maximus sagittis. Duis id libero eu mi vulputate congue id \
633 eu est. Maecenas pellentesque massa id dui fringilla, et porta nulla imperdiet. \
634 Sed eu est rutrum, scelerisque tortor ac, lacinia turpis.";
635
636 callback.call(
637 &StyledBox {
638 fill: Some(0x00_00_FF_FF),
639 ..StyledBox::new(Row::new(|content| {
640 content.add(&Text::basic(text_a, &font, 24.), Flex::Expand(1));
641 content.add(&Text::basic(text_b, &font, 24.), Flex::Expand(1));
642 }))
643 }
644 .debug(0)
645 .show_max_width()
646 .show_last_location_max_height(),
647 );
648 });
649 assert_binary_snapshot!(".pdf", bytes);
650 }
651}