1use crate::{
10 AlignmentGeometry, BlendMode, BoxFit, Color, ColorFilter, ImageHandle, LayerFilter, Primitive,
11 Rect, Scene, Size, TextDirection,
12};
13
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
17pub enum ImageRepeat {
18 Repeat,
20 RepeatX,
22 RepeatY,
24 #[default]
26 NoRepeat,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq)]
31pub struct FittedSizes {
32 pub source: Size,
34 pub destination: Size,
36}
37
38pub fn apply_box_fit(fit: BoxFit, input: Size, output: Size) -> FittedSizes {
45 let zero = Size::new(0.0, 0.0);
46 if input.width <= 0.0 || input.height <= 0.0 || output.width <= 0.0 || output.height <= 0.0 {
47 return FittedSizes {
48 source: zero,
49 destination: zero,
50 };
51 }
52 let wider = output.width / output.height > input.width / input.height;
54 let contain = || {
56 if wider {
57 Size::new(input.width * output.height / input.height, output.height)
58 } else {
59 Size::new(output.width, input.height * output.width / input.width)
60 }
61 };
62 let cover = || {
64 if wider {
65 Size::new(input.width, input.width * output.height / output.width)
66 } else {
67 Size::new(input.height * output.width / output.height, input.height)
68 }
69 };
70 let (source, destination) = match fit {
71 BoxFit::Fill => (input, output),
72 BoxFit::Contain => (input, contain()),
73 BoxFit::Cover => (cover(), output),
74 BoxFit::FitWidth if wider => (cover(), output),
75 BoxFit::FitWidth => (input, contain()),
76 BoxFit::FitHeight if wider => (input, contain()),
77 BoxFit::FitHeight => (cover(), output),
78 BoxFit::None => {
79 let shown = Size::new(
80 input.width.min(output.width),
81 input.height.min(output.height),
82 );
83 (shown, shown)
84 }
85 BoxFit::ScaleDown => {
86 let aspect = input.width / input.height;
87 let mut size = input;
88 if size.height > output.height {
89 size = Size::new(output.height * aspect, output.height);
90 }
91 if size.width > output.width {
92 size = Size::new(output.width, output.width / aspect);
93 }
94 (input, size)
95 }
96 };
97 FittedSizes {
98 source,
99 destination,
100 }
101}
102
103pub const MAX_IMAGE_TILES: usize = 10_000;
106
107#[derive(Clone, Copy, Debug, PartialEq)]
110pub struct ImagePiece {
111 pub dst: Rect,
113 pub uv: Rect,
115 pub strict: bool,
118}
119
120#[derive(Clone, Debug, PartialEq)]
130pub struct DecorationImage {
131 pub image: Option<ImageHandle>,
133 pub color_filter: Option<ColorFilter>,
135 pub fit: Option<BoxFit>,
137 pub alignment: AlignmentGeometry,
139 pub center_slice: Option<Rect>,
144 pub repeat: ImageRepeat,
146 pub match_text_direction: bool,
148 pub scale: f32,
150 pub opacity: f32,
152 pub invert_colors: bool,
154 crossfade: Option<Box<Crossfade>>,
156}
157
158#[derive(Clone, Debug, PartialEq)]
160struct Crossfade {
161 from: Option<DecorationImage>,
162 to: Option<DecorationImage>,
163 t: f32,
164}
165
166impl DecorationImage {
167 pub fn new(image: impl Into<Option<ImageHandle>>) -> Self {
169 Self {
170 image: image.into(),
171 color_filter: None,
172 fit: None,
173 alignment: crate::Alignment::CENTER.into(),
174 center_slice: None,
175 repeat: ImageRepeat::NoRepeat,
176 match_text_direction: false,
177 scale: 1.0,
178 opacity: 1.0,
179 invert_colors: false,
180 crossfade: None,
181 }
182 }
183
184 pub fn color_filter(mut self, filter: ColorFilter) -> Self {
186 self.color_filter = Some(filter);
187 self
188 }
189
190 pub fn fit(mut self, fit: BoxFit) -> Self {
192 self.fit = Some(fit);
193 self
194 }
195
196 pub fn alignment(mut self, alignment: impl Into<AlignmentGeometry>) -> Self {
198 self.alignment = alignment.into();
199 self
200 }
201
202 pub fn center_slice(mut self, slice: Rect) -> Self {
204 self.center_slice = Some(slice);
205 self
206 }
207
208 pub fn repeat(mut self, repeat: ImageRepeat) -> Self {
210 self.repeat = repeat;
211 self
212 }
213
214 pub fn match_text_direction(mut self, matches: bool) -> Self {
216 self.match_text_direction = matches;
217 self
218 }
219
220 pub fn scale(mut self, scale: f32) -> Self {
222 self.scale = scale;
223 self
224 }
225
226 pub fn opacity(mut self, opacity: f32) -> Self {
228 self.opacity = opacity;
229 self
230 }
231
232 pub fn invert_colors(mut self, invert: bool) -> Self {
234 self.invert_colors = invert;
235 self
236 }
237
238 pub fn lerp(
243 a: Option<&DecorationImage>,
244 b: Option<&DecorationImage>,
245 t: f32,
246 ) -> Option<DecorationImage> {
247 if t.is_nan() || t <= 0.0 || a == b {
248 return a.cloned();
249 }
250 if t >= 1.0 {
251 return b.cloned();
252 }
253 let shown = b.or(a)?.clone();
256 Some(DecorationImage {
257 crossfade: Some(Box::new(Crossfade {
258 from: a.cloned(),
259 to: b.cloned(),
260 t,
261 })),
262 ..shown
263 })
264 }
265
266 pub fn pieces(
274 &self,
275 size: Size,
276 rect: Rect,
277 direction: TextDirection,
278 ) -> (Vec<ImagePiece>, bool) {
279 let none = (Vec::new(), false);
280 if rect.width <= 0.0 || rect.height <= 0.0 || size.width <= 0.0 || size.height <= 0.0 {
281 return none;
282 }
283 let scale = if self.scale > 0.0 { self.scale } else { 1.0 };
284 let mut output = Size::new(rect.width, rect.height);
285 let mut input = size;
286 let border = self.center_slice.map(|slice| {
288 Size::new(
289 size.width / scale - slice.width,
290 size.height / scale - slice.height,
291 )
292 });
293 if let Some(b) = border {
294 output = Size::new(output.width - b.width, output.height - b.height);
295 input = Size::new(
296 input.width - b.width * scale,
297 input.height - b.height * scale,
298 );
299 }
300 let fit = self.fit.unwrap_or(if self.center_slice.is_none() {
301 BoxFit::ScaleDown
302 } else {
303 BoxFit::Fill
304 });
305 let fitted = apply_box_fit(
306 fit,
307 Size::new(input.width / scale, input.height / scale),
308 output,
309 );
310 let source = Size::new(fitted.source.width * scale, fitted.source.height * scale);
311 let mut destination = fitted.destination;
312 if let Some(b) = border {
313 output = Size::new(output.width + b.width, output.height + b.height);
314 destination = Size::new(destination.width + b.width, destination.height + b.height);
315 }
316 if destination.width <= 0.0 || destination.height <= 0.0 {
317 return none;
318 }
319 let repeat = if destination == output {
321 ImageRepeat::NoRepeat
322 } else {
323 self.repeat
324 };
325 let flip = self.match_text_direction && direction.is_rtl();
326 let alignment = self.alignment.resolve(direction);
327 let half_w = (output.width - destination.width) / 2.0;
331 let half_h = (output.height - destination.height) / 2.0;
332 let ax = if flip { -alignment.x } else { alignment.x };
333 let placed = Rect::new(
334 rect.x + half_w + ax * half_w,
335 rect.y + half_h + alignment.y * half_h,
336 destination.width,
337 destination.height,
338 );
339 let tiles = if repeat == ImageRepeat::NoRepeat {
340 vec![placed]
341 } else {
342 tile_rects(rect, placed, repeat)
343 };
344 let mut pieces = Vec::new();
345 match self.center_slice {
346 None => {
347 let x = (size.width - source.width) / 2.0 * (1.0 + alignment.x);
349 let y = (size.height - source.height) / 2.0 * (1.0 + alignment.y);
350 let uv = Rect::new(
351 x / size.width,
352 y / size.height,
353 source.width / size.width,
354 source.height / size.height,
355 );
356 pieces.extend(tiles.into_iter().map(|dst| ImagePiece {
357 dst,
358 uv,
359 strict: false,
360 }));
361 }
362 Some(slice) => {
363 for tile in tiles {
364 nine(&mut pieces, size, slice, scale, tile);
365 }
366 }
367 }
368 if flip {
369 let across = 2.0 * rect.x + rect.width;
372 for piece in &mut pieces {
373 let d = piece.dst;
374 piece.dst = Rect::new(across - d.x - d.width, d.y, d.width, d.height);
375 let uv = piece.uv;
376 piece.uv = Rect::new(uv.x + uv.width, uv.y, -uv.width, uv.height);
377 }
378 }
379 (pieces, repeat != ImageRepeat::NoRepeat)
380 }
381
382 pub fn paint(&self, scene: &mut Scene, rect: Rect, direction: TextDirection, blend: f32) {
386 if let Some(crossfade) = &self.crossfade {
387 crossfade.paint(scene, rect, direction, blend);
388 return;
389 }
390 let Some(image) = &self.image else {
391 return;
392 };
393 let alpha = (self.opacity * blend).clamp(0.0, 1.0);
394 if alpha <= 0.0 {
395 return;
396 }
397 let (pieces, repeats) = self.pieces(image.size(), rect, direction);
398 if pieces.is_empty() {
399 return;
400 }
401 let start = scene.primitives().len();
402 let outer = scene.current_clip();
403 if repeats {
404 scene.set_clip(outer.intersect(rect));
405 }
406 let tint = Color::WHITE.fade(alpha);
407 for piece in pieces {
408 if piece.strict {
409 scene.draw_image_strict(image, piece.dst, piece.uv, tint);
410 } else {
411 scene.draw_image(image, piece.dst, piece.uv, tint);
412 }
413 }
414 scene.set_clip(outer);
415 if let Some(filter) = self.color_filter {
418 group(scene, start, 1.0, Some(filter));
419 }
420 if self.invert_colors {
421 group(scene, start, 1.0, Some(ColorFilter::invert()));
422 }
423 }
424}
425
426impl Crossfade {
427 fn paint(&self, scene: &mut Scene, rect: Rect, direction: TextDirection, blend: f32) {
429 match (&self.from, &self.to) {
430 (Some(from), Some(to)) => {
431 let start = scene.primitives().len();
435 from.paint(scene, rect, direction, blend * (1.0 - self.t));
436 scene.blended(BlendMode::Plus, |scene| {
437 to.paint(scene, rect, direction, blend * self.t);
438 });
439 group(scene, start, 1.0, None);
440 }
441 (Some(from), None) => from.paint(scene, rect, direction, blend * (1.0 - self.t)),
442 (None, Some(to)) => to.paint(scene, rect, direction, blend * self.t),
443 (None, None) => {}
444 }
445 }
446}
447
448fn group(scene: &mut Scene, start: usize, opacity: f32, filter: Option<ColorFilter>) {
450 if scene.primitives().len() == start {
451 return;
452 }
453 let primitives = scene.split_off(start);
454 let layer = Primitive::Layer {
455 primitives,
456 opacity,
457 clip: scene.current_clip(),
458 clip_shape: crate::ClipShape::Rect,
459 transform: None,
460 filter: LayerFilter {
461 color: filter,
462 ..LayerFilter::NONE
463 },
464 owner: scene.current_owner(),
465 };
466 scene.push_primitive(layer);
467}
468
469fn tile_rects(rect: Rect, placed: Rect, repeat: ImageRepeat) -> Vec<Rect> {
472 let (stride_x, stride_y) = (placed.width, placed.height);
473 let across = matches!(repeat, ImageRepeat::Repeat | ImageRepeat::RepeatX);
474 let down = matches!(repeat, ImageRepeat::Repeat | ImageRepeat::RepeatY);
475 let (mut start_x, mut stop_x, mut start_y, mut stop_y) = (0i64, 0i64, 0i64, 0i64);
476 if across {
477 start_x = ((rect.x - placed.x) / stride_x).floor() as i64;
478 stop_x = ((rect.x + rect.width - placed.x - placed.width) / stride_x).ceil() as i64;
479 }
480 if down {
481 start_y = ((rect.y - placed.y) / stride_y).floor() as i64;
482 stop_y = ((rect.y + rect.height - placed.y - placed.height) / stride_y).ceil() as i64;
483 }
484 let mut tiles = Vec::new();
485 'columns: for i in start_x..=stop_x {
486 for j in start_y..=stop_y {
487 if tiles.len() == MAX_IMAGE_TILES {
488 break 'columns;
489 }
490 tiles.push(Rect::new(
491 placed.x + i as f32 * stride_x,
492 placed.y + j as f32 * stride_y,
493 placed.width,
494 placed.height,
495 ));
496 }
497 }
498 tiles
499}
500
501fn nine(pieces: &mut Vec<ImagePiece>, size: Size, slice: Rect, scale: f32, dst: Rect) {
507 let sx = [
509 0.0,
510 (slice.x * scale).clamp(0.0, size.width),
511 ((slice.x + slice.width) * scale).clamp(0.0, size.width),
512 size.width,
513 ];
514 let sy = [
515 0.0,
516 (slice.y * scale).clamp(0.0, size.height),
517 ((slice.y + slice.height) * scale).clamp(0.0, size.height),
518 size.height,
519 ];
520 let (left, right) = (sx[1] / scale, (sx[3] - sx[2]) / scale);
522 let (top, bottom) = (sy[1] / scale, (sy[3] - sy[2]) / scale);
523 let dx = [
524 dst.x,
525 dst.x + left,
526 dst.x + dst.width - right,
527 dst.x + dst.width,
528 ];
529 let dy = [
530 dst.y,
531 dst.y + top,
532 dst.y + dst.height - bottom,
533 dst.y + dst.height,
534 ];
535 for j in 0..3 {
536 for i in 0..3 {
537 let (w, h) = (dx[i + 1] - dx[i], dy[j + 1] - dy[j]);
538 let (sw, sh) = (sx[i + 1] - sx[i], sy[j + 1] - sy[j]);
539 if w <= 0.0 || h <= 0.0 || sw <= 0.0 || sh <= 0.0 {
540 continue;
541 }
542 pieces.push(ImagePiece {
543 dst: Rect::new(dx[i], dy[j], w, h),
544 uv: Rect::new(
545 sx[i] / size.width,
546 sy[j] / size.height,
547 sw / size.width,
548 sh / size.height,
549 ),
550 strict: true,
551 });
552 }
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use crate::{Alignment, AlignmentDirectional, ImageData};
560
561 fn handle(width: u32, height: u32) -> ImageHandle {
562 ImageData::from_rgba(width, height, vec![255; (width * height * 4) as usize]).into_handle()
563 }
564
565 fn size(width: f32, height: f32) -> Size {
566 Size::new(width, height)
567 }
568
569 fn fitted(fit: BoxFit, input: Size, output: Size) -> (Size, Size) {
570 let f = apply_box_fit(fit, input, output);
571 (f.source, f.destination)
572 }
573
574 fn pieces(image: &DecorationImage, px: Size, rect: Rect) -> Vec<ImagePiece> {
575 image.pieces(px, rect, TextDirection::Ltr).0
576 }
577
578 #[test]
580 fn a_fit_settles_the_reference_sizes() {
581 let (input, square) = (size(200.0, 100.0), size(100.0, 100.0));
582 assert_eq!(fitted(BoxFit::Fill, input, square), (input, square));
583 assert_eq!(
584 fitted(BoxFit::Contain, input, square),
585 (input, size(100.0, 50.0))
586 );
587 assert_eq!(
588 fitted(BoxFit::Cover, input, square),
589 (size(100.0, 100.0), square)
590 );
591 assert_eq!(
592 fitted(BoxFit::FitWidth, input, square),
593 (input, size(100.0, 50.0))
594 );
595 assert_eq!(
596 fitted(BoxFit::FitHeight, input, square),
597 (size(100.0, 100.0), square)
598 );
599 assert_eq!(fitted(BoxFit::None, input, square), (square, square));
600 assert_eq!(
601 fitted(BoxFit::ScaleDown, input, square),
602 (input, size(100.0, 50.0))
603 );
604 let (picture, band) = (size(100.0, 100.0), size(200.0, 50.0));
607 assert_eq!(
608 fitted(BoxFit::FitWidth, picture, band),
609 (size(100.0, 25.0), band)
610 );
611 assert_eq!(
612 fitted(BoxFit::FitHeight, picture, band),
613 (picture, size(50.0, 50.0))
614 );
615 assert_eq!(
616 fitted(BoxFit::Contain, picture, band),
617 (picture, size(50.0, 50.0))
618 );
619 assert_eq!(
620 fitted(BoxFit::Cover, picture, band),
621 (size(100.0, 25.0), band)
622 );
623 assert_eq!(
625 fitted(BoxFit::ScaleDown, size(100.0, 400.0), size(50.0, 100.0)),
626 (size(100.0, 400.0), size(25.0, 100.0))
627 );
628 assert_eq!(
629 fitted(BoxFit::ScaleDown, size(400.0, 100.0), size(100.0, 100.0)),
630 (size(400.0, 100.0), size(100.0, 25.0))
631 );
632 assert_eq!(
634 fitted(BoxFit::ScaleDown, size(10.0, 10.0), square),
635 (size(10.0, 10.0), size(10.0, 10.0))
636 );
637 }
638
639 #[test]
640 fn an_empty_size_fits_nothing() {
641 let zero = size(0.0, 0.0);
642 let ten = size(10.0, 10.0);
643 assert_eq!(fitted(BoxFit::Fill, zero, ten), (zero, zero));
644 assert_eq!(fitted(BoxFit::Fill, size(10.0, 0.0), ten), (zero, zero));
645 assert_eq!(fitted(BoxFit::Fill, ten, size(10.0, 0.0)), (zero, zero));
646 assert_eq!(fitted(BoxFit::Fill, ten, size(0.0, 10.0)), (zero, zero));
647 }
648
649 #[test]
651 fn a_small_picture_sits_in_the_middle() {
652 let image = DecorationImage::new(handle(10, 10));
653 let got = pieces(&image, size(10.0, 10.0), Rect::new(0.0, 0.0, 100.0, 50.0));
654 assert_eq!(
655 got,
656 vec![ImagePiece {
657 dst: Rect::new(45.0, 20.0, 10.0, 10.0),
658 uv: Rect::new(0.0, 0.0, 1.0, 1.0),
659 strict: false,
660 }]
661 );
662 let corner = image.alignment(Alignment::TOP_LEFT);
663 let got = pieces(&corner, size(10.0, 10.0), Rect::new(5.0, 5.0, 100.0, 50.0));
664 assert_eq!(got[0].dst, Rect::new(5.0, 5.0, 10.0, 10.0));
665 let empty = pieces(&corner, size(10.0, 10.0), Rect::new(5.0, 5.0, 0.0, 50.0));
666 assert!(empty.is_empty(), "an empty box draws nothing");
667 }
668
669 #[test]
671 fn a_cover_keeps_the_aligned_part() {
672 let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
673 let wide = size(20.0, 10.0);
674 let left = DecorationImage::new(handle(20, 10))
675 .fit(BoxFit::Cover)
676 .alignment(Alignment::CENTER_LEFT);
677 assert_eq!(
678 pieces(&left, wide, rect)[0].uv,
679 Rect::new(0.0, 0.0, 0.5, 1.0)
680 );
681 let right = left.clone().alignment(Alignment::CENTER_RIGHT);
682 assert_eq!(
683 pieces(&right, wide, rect)[0].uv,
684 Rect::new(0.5, 0.0, 0.5, 1.0)
685 );
686 let centre = left.alignment(Alignment::CENTER);
687 assert_eq!(
688 pieces(¢re, wide, rect)[0].uv,
689 Rect::new(0.25, 0.0, 0.5, 1.0)
690 );
691 let tall = DecorationImage::new(handle(10, 20))
692 .fit(BoxFit::Cover)
693 .alignment(Alignment::BOTTOM_CENTER);
694 assert_eq!(
695 pieces(&tall, size(10.0, 20.0), rect)[0].uv,
696 Rect::new(0.0, 0.5, 1.0, 0.5)
697 );
698 }
699
700 #[test]
703 fn a_repeated_picture_covers_its_axis() {
704 let image = DecorationImage::new(handle(10, 10)).repeat(ImageRepeat::RepeatX);
705 let rect = Rect::new(0.0, 0.0, 25.0, 30.0);
706 let (got, clipped) = image.pieces(size(10.0, 10.0), rect, TextDirection::Ltr);
707 let xs: Vec<f32> = got.iter().map(|p| p.dst.x).collect();
708 assert_eq!(xs, vec![-2.5, 7.5, 17.5]);
709 assert!(got.iter().all(|p| p.dst.y == 10.0), "across only");
710 assert!(clipped, "a repeat is clipped to the box");
711 let both = image.repeat(ImageRepeat::Repeat);
712 assert_eq!(
713 pieces(&both, size(10.0, 10.0), rect).len(),
714 9,
715 "three by three"
716 );
717 let down = both.repeat(ImageRepeat::RepeatY);
718 let got = pieces(&down, size(10.0, 10.0), rect);
719 let ys: Vec<f32> = got.iter().map(|p| p.dst.y).collect();
720 assert_eq!(ys, vec![0.0, 10.0, 20.0]);
721 assert!(got.iter().all(|p| p.dst.x == 7.5), "down only");
722 let once = down.repeat(ImageRepeat::NoRepeat);
723 let (got, clipped) = once.pieces(size(10.0, 10.0), rect, TextDirection::Ltr);
724 assert_eq!(got.len(), 1);
725 assert!(!clipped);
726 }
727
728 #[test]
730 fn a_picture_filling_its_box_is_drawn_once() {
731 let image = DecorationImage::new(handle(10, 10))
732 .fit(BoxFit::Fill)
733 .repeat(ImageRepeat::Repeat);
734 let rect = Rect::new(0.0, 0.0, 40.0, 40.0);
735 let (got, clipped) = image.pieces(size(10.0, 10.0), rect, TextDirection::Ltr);
736 assert_eq!(got.len(), 1);
737 assert!(!clipped);
738 }
739
740 #[test]
741 fn a_tiny_pattern_stops_at_the_tile_limit() {
742 let image = DecorationImage::new(handle(1, 1)).repeat(ImageRepeat::Repeat);
743 let got = pieces(&image, size(1.0, 1.0), Rect::new(0.0, 0.0, 200.0, 200.0));
744 assert_eq!(got.len(), MAX_IMAGE_TILES);
745 }
746
747 fn sliced() -> DecorationImage {
748 DecorationImage::new(handle(30, 30)).center_slice(Rect::new(10.0, 10.0, 10.0, 10.0))
749 }
750
751 #[test]
754 fn a_centre_slice_stretches_the_middle_only() {
755 let got = pieces(
756 &sliced(),
757 size(30.0, 30.0),
758 Rect::new(0.0, 0.0, 100.0, 60.0),
759 );
760 assert_eq!(got.len(), 9);
761 let third = 1.0 / 3.0;
762 assert_eq!(
763 got[0].dst,
764 Rect::new(0.0, 0.0, 10.0, 10.0),
765 "a corner, its own size"
766 );
767 assert_eq!(got[0].uv, Rect::new(0.0, 0.0, third, third));
768 assert_eq!(
769 got[1].dst,
770 Rect::new(10.0, 0.0, 80.0, 10.0),
771 "an edge, along its length"
772 );
773 assert_eq!(
774 got[4].dst,
775 Rect::new(10.0, 10.0, 80.0, 40.0),
776 "the centre, stretched"
777 );
778 assert_eq!(got[4].uv, Rect::new(third, third, third, third));
779 assert!(
780 got.iter().all(|p| p.strict),
781 "each part sampled within itself"
782 );
783 assert_eq!(got[8].dst, Rect::new(90.0, 50.0, 10.0, 10.0));
784 assert_eq!(got[8].uv, Rect::new(2.0 * third, 2.0 * third, third, third));
785 }
786
787 #[test]
791 fn a_slice_in_a_small_box_keeps_its_corners() {
792 let got = pieces(&sliced(), size(30.0, 30.0), Rect::new(0.0, 0.0, 10.0, 10.0));
793 let dsts: Vec<Rect> = got.iter().map(|p| p.dst).collect();
794 assert_eq!(
795 dsts,
796 vec![
797 Rect::new(-5.0, -5.0, 10.0, 10.0),
798 Rect::new(5.0, -5.0, 10.0, 10.0),
799 Rect::new(-5.0, 5.0, 10.0, 10.0),
800 Rect::new(5.0, 5.0, 10.0, 10.0),
801 ]
802 );
803 }
804
805 #[test]
807 fn a_repeated_slice_is_sliced_per_copy() {
808 let image = sliced().fit(BoxFit::Contain).repeat(ImageRepeat::RepeatX);
809 let got = pieces(&image, size(30.0, 30.0), Rect::new(0.0, 0.0, 90.0, 30.0));
810 assert_eq!(got.len(), 27, "three copies of nine");
811 }
812
813 #[test]
815 fn a_scaled_picture_is_drawn_smaller() {
816 let image = DecorationImage::new(handle(20, 20))
817 .scale(2.0)
818 .alignment(Alignment::TOP_LEFT);
819 let got = pieces(&image, size(20.0, 20.0), Rect::new(0.0, 0.0, 100.0, 100.0));
820 assert_eq!(got[0].dst, Rect::new(0.0, 0.0, 10.0, 10.0));
821 let sliced = DecorationImage::new(handle(30, 30))
822 .scale(3.0)
823 .center_slice(Rect::new(2.0, 2.0, 6.0, 6.0));
824 let got = pieces(&sliced, size(30.0, 30.0), Rect::new(0.0, 0.0, 40.0, 40.0));
825 assert_eq!(
826 got[0].dst,
827 Rect::new(0.0, 0.0, 2.0, 2.0),
828 "a corner at its scaled size"
829 );
830 assert_eq!(got[0].uv, Rect::new(0.0, 0.0, 0.2, 0.2));
831 assert_eq!(got[8].dst, Rect::new(38.0, 38.0, 2.0, 2.0));
832 }
833
834 #[test]
837 fn a_directional_picture_is_mirrored_in_rtl() {
838 let image = DecorationImage::new(handle(10, 10))
839 .alignment(Alignment::TOP_LEFT)
840 .match_text_direction(true);
841 let rect = Rect::new(20.0, 0.0, 100.0, 50.0);
842 let ltr = image.pieces(size(10.0, 10.0), rect, TextDirection::Ltr).0;
843 assert_eq!(ltr[0].uv, Rect::new(0.0, 0.0, 1.0, 1.0));
844 let rtl = image.pieces(size(10.0, 10.0), rect, TextDirection::Rtl).0;
845 assert_eq!(rtl[0].dst, Rect::new(20.0, 0.0, 10.0, 10.0));
846 assert_eq!(rtl[0].uv, Rect::new(1.0, 0.0, -1.0, 1.0));
847 let start = DecorationImage::new(handle(10, 10)).alignment(AlignmentDirectional::TOP_START);
849 let rtl = start.pieces(size(10.0, 10.0), rect, TextDirection::Rtl).0;
850 assert_eq!(rtl[0].dst, Rect::new(110.0, 0.0, 10.0, 10.0));
851 assert_eq!(
852 rtl[0].uv,
853 Rect::new(0.0, 0.0, 1.0, 1.0),
854 "not mirrored unless asked"
855 );
856 }
857
858 #[test]
860 fn a_mirrored_slice_swaps_its_sides() {
861 let image = DecorationImage::new(handle(30, 10))
862 .center_slice(Rect::new(5.0, 0.0, 5.0, 10.0))
863 .match_text_direction(true);
864 let rect = Rect::new(0.0, 0.0, 100.0, 10.0);
865 let got = image.pieces(size(30.0, 10.0), rect, TextDirection::Rtl).0;
866 assert_eq!(got[0].dst, Rect::new(95.0, 0.0, 5.0, 10.0));
868 assert_eq!(got[0].uv, Rect::new(5.0 / 30.0, 0.0, -5.0 / 30.0, 1.0));
869 assert_eq!(got[2].dst, Rect::new(0.0, 0.0, 20.0, 10.0));
871 }
872
873 fn painted(image: &DecorationImage, blend: f32) -> Scene {
874 let mut scene = Scene::new();
875 image.paint(
876 &mut scene,
877 Rect::new(0.0, 0.0, 40.0, 40.0),
878 TextDirection::Ltr,
879 blend,
880 );
881 scene
882 }
883
884 fn tints(primitives: &[Primitive]) -> Vec<f32> {
885 primitives
886 .iter()
887 .flat_map(|p| match p {
888 Primitive::Image { tint, .. } => vec![tint.a],
889 Primitive::Layer { primitives, .. } => tints(primitives),
890 _ => vec![],
891 })
892 .collect()
893 }
894
895 #[test]
896 fn a_picture_paints_at_its_opacity() {
897 let image = DecorationImage::new(handle(10, 10)).opacity(0.5);
898 assert_eq!(tints(painted(&image, 0.5).primitives()), vec![0.25]);
899 assert!(painted(&DecorationImage::new(None), 1.0)
900 .primitives()
901 .is_empty());
902 assert!(painted(&image.opacity(0.0), 1.0).primitives().is_empty());
903 }
904
905 #[test]
907 fn the_filter_comes_before_the_inversion() {
908 let filtered = DecorationImage::new(handle(10, 10))
909 .color_filter(ColorFilter::grayscale())
910 .invert_colors(true);
911 let scene = painted(&filtered, 1.0);
912 let Primitive::Layer {
913 primitives, filter, ..
914 } = &scene.primitives()[0]
915 else {
916 panic!("a layer");
917 };
918 assert_eq!(filter.color, Some(ColorFilter::invert()), "inverted last");
919 let Primitive::Layer { filter, .. } = &primitives[0] else {
920 panic!("the filter's layer, inside");
921 };
922 assert_eq!(filter.color, Some(ColorFilter::grayscale()));
923 let plain = painted(&DecorationImage::new(handle(10, 10)), 1.0);
924 assert!(
925 matches!(plain.primitives()[0], Primitive::Image { .. }),
926 "no layer for nothing"
927 );
928 let inverted = painted(
929 &DecorationImage::new(handle(10, 10)).invert_colors(true),
930 1.0,
931 );
932 assert!(
933 matches!(&inverted.primitives()[0], Primitive::Layer { filter, .. } if filter.color == Some(ColorFilter::invert()))
934 );
935 }
936
937 #[test]
938 fn a_repeated_picture_is_clipped_to_its_box() {
939 let mut scene = Scene::new();
940 scene.set_clip(Rect::new(0.0, 0.0, 30.0, 300.0));
941 DecorationImage::new(handle(10, 10))
942 .repeat(ImageRepeat::Repeat)
943 .paint(
944 &mut scene,
945 Rect::new(5.0, 5.0, 40.0, 40.0),
946 TextDirection::Ltr,
947 1.0,
948 );
949 assert_eq!(scene.primitives().len(), 25, "five by five");
950 for p in scene.primitives() {
951 let Primitive::Image { clip, .. } = p else {
952 panic!("tiles");
953 };
954 assert_eq!(*clip, Rect::new(5.0, 5.0, 25.0, 40.0));
955 }
956 assert_eq!(
957 scene.current_clip(),
958 Rect::new(0.0, 0.0, 30.0, 300.0),
959 "restored"
960 );
961 }
962
963 #[test]
966 fn two_pictures_cross_over() {
967 let a = DecorationImage::new(handle(10, 10));
968 let b = DecorationImage::new(handle(12, 12)).opacity(0.8);
969 assert_eq!(
970 DecorationImage::lerp(Some(&a), Some(&b), 0.0),
971 Some(a.clone())
972 );
973 assert_eq!(
974 DecorationImage::lerp(Some(&a), Some(&b), 1.0),
975 Some(b.clone())
976 );
977 assert_eq!(
978 DecorationImage::lerp(Some(&a), Some(&a), 0.5),
979 Some(a.clone())
980 );
981 let mid = DecorationImage::lerp(Some(&a), Some(&b), 0.25).unwrap();
982 assert_eq!(mid.opacity, 0.8, "the arriving picture's settings");
983 assert_eq!(tints(painted(&mid, 1.0).primitives()), vec![0.75, 0.2]);
984 assert_eq!(tints(painted(&mid, 0.5).primitives()), vec![0.375, 0.1]);
985 let scene = painted(&mid, 1.0);
987 let [Primitive::Layer { primitives, .. }] = scene.primitives() else {
988 panic!("one group");
989 };
990 assert!(
991 matches!(primitives[0], Primitive::Image { .. }),
992 "the leaving picture"
993 );
994 assert!(
995 matches!(&primitives[1], Primitive::Layer { filter, .. } if filter.blend == Some(BlendMode::Plus)),
996 "the arriving one, added"
997 );
998 let leaving = DecorationImage::lerp(Some(&a), None, 0.25).unwrap();
999 assert_eq!(tints(painted(&leaving, 1.0).primitives()), vec![0.75]);
1000 let arriving = DecorationImage::lerp(None, Some(&a), 0.25).unwrap();
1001 assert_eq!(tints(painted(&arriving, 1.0).primitives()), vec![0.25]);
1002 assert_eq!(DecorationImage::lerp(None, None, 0.5), None);
1003 }
1004}