Skip to main content

frus_core/
decoration_image.rs

1//! **A picture as a box's background** (milestone 645): the reference's `DecorationImage`
2//! and the `paintImage` that draws it (`decoration_image.dart`).
3//!
4//! A [`BoxDecoration`](crate::BoxDecoration) paints it between its background and its
5//! border, clipped to its corners or its circle. Everything about where the picture
6//! goes is here: how it is fitted, which part of it shows, where the spare room goes,
7//! whether it repeats, which part of it stretches, and whether it is mirrored.
8
9use crate::{
10    AlignmentGeometry, BlendMode, BoxFit, Color, ColorFilter, ImageHandle, LayerFilter, Primitive,
11    Rect, Scene, Size, TextDirection,
12};
13
14/// How a picture fills the room its fit leaves: once, or repeated across, down, or both
15/// — the reference's `ImageRepeat`.
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
17pub enum ImageRepeat {
18    /// Across and down, until the box is covered.
19    Repeat,
20    /// Across only.
21    RepeatX,
22    /// Down only.
23    RepeatY,
24    /// Once; the rest of the box stays empty.
25    #[default]
26    NoRepeat,
27}
28
29/// The two sizes a fit settles on — the reference's `FittedSizes`.
30#[derive(Clone, Copy, Debug, PartialEq)]
31pub struct FittedSizes {
32    /// How much of the picture shows, in the picture's own units.
33    pub source: Size,
34    /// How big it is drawn.
35    pub destination: Size,
36}
37
38/// Fits `input` into `output` by `fit` — the reference's `applyBoxFit`.
39///
40/// Unlike [`BoxFit::apply`], nothing is ever drawn bigger than `output`: a fit that would
41/// overflow shows **less of the picture** instead. [`BoxFit::None`] shows as much of it
42/// as fits at its own size, and [`BoxFit::FitWidth`] on a picture taller than its box
43/// shows a band of it. An empty size on either side fits nothing.
44pub 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    // Whether the box is wider, for its height, than the picture.
53    let wider = output.width / output.height > input.width / input.height;
54    // The whole picture, as large as fits.
55    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    // The part of the picture the box's shape cuts out of it.
63    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
103/// The most tiles one repeated picture draws. A one-pixel pattern repeated over a large
104/// box would otherwise be millions of draws; past this, the rest of the box stays empty.
105pub const MAX_IMAGE_TILES: usize = 10_000;
106
107/// One part of a picture drawn somewhere: where, and which part, in `0..1` of the
108/// picture (a negative width walks it backwards, which is how a picture is mirrored).
109#[derive(Clone, Copy, Debug, PartialEq)]
110pub struct ImagePiece {
111    /// Where it lands.
112    pub dst: Rect,
113    /// The part of the picture sampled.
114    pub uv: Rect,
115    /// Whether sampling stays strictly inside `uv`: the parts of a sliced picture,
116    /// stretched apart from each other.
117    pub strict: bool,
118}
119
120/// A picture painted as a box's background — the reference's `DecorationImage`.
121///
122/// The defaults are the reference's: centred, not repeated, no slice, at its own scale
123/// and opacity, fitted by [`BoxFit::ScaleDown`] (or [`BoxFit::Fill`] with a centre slice)
124/// when no fit is named.
125///
126/// The picture is pixels already decoded. One that is still on its way — a network
127/// picture, say — is `None` for now, and paints nothing until it arrives, as the
128/// reference's does.
129#[derive(Clone, Debug, PartialEq)]
130pub struct DecorationImage {
131    /// The pixels; `None` paints nothing.
132    pub image: Option<ImageHandle>,
133    /// A colour filter over the picture, applied before its opacity is.
134    pub color_filter: Option<ColorFilter>,
135    /// How the picture is fitted into the box; `None` is the reference's default.
136    pub fit: Option<BoxFit>,
137    /// Where the picture sits in the spare room, and which part of it a crop keeps.
138    pub alignment: AlignmentGeometry,
139    /// The **centre slice**, in the picture's units (its pixels over `scale`): the part
140    /// that stretches, while the four corners stay their size and the four edges
141    /// stretch along their length. Fits that crop ([`BoxFit::None`],
142    /// [`BoxFit::Cover`]) have nothing to stretch and are not meant for it.
143    pub center_slice: Option<Rect>,
144    /// Whether the picture repeats to fill the box.
145    pub repeat: ImageRepeat,
146    /// Whether the picture is mirrored when the box reads right to left.
147    pub match_text_direction: bool,
148    /// The picture's pixels per logical pixel: `2.0` draws it at half its pixel size.
149    pub scale: f32,
150    /// How opaque the picture is, `0..=1`.
151    pub opacity: f32,
152    /// Whether the picture's colours are inverted, after the colour filter.
153    pub invert_colors: bool,
154    /// Two pictures mid-transition, painted one over the other.
155    crossfade: Option<Box<Crossfade>>,
156}
157
158/// Two pictures `t` of the way from one to the other.
159#[derive(Clone, Debug, PartialEq)]
160struct Crossfade {
161    from: Option<DecorationImage>,
162    to: Option<DecorationImage>,
163    t: f32,
164}
165
166impl DecorationImage {
167    /// The picture `image`, centred, at its own size or smaller.
168    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    /// Sets the colour filter.
185    pub fn color_filter(mut self, filter: ColorFilter) -> Self {
186        self.color_filter = Some(filter);
187        self
188    }
189
190    /// Sets the fit.
191    pub fn fit(mut self, fit: BoxFit) -> Self {
192        self.fit = Some(fit);
193        self
194    }
195
196    /// Sets where the picture sits.
197    pub fn alignment(mut self, alignment: impl Into<AlignmentGeometry>) -> Self {
198        self.alignment = alignment.into();
199        self
200    }
201
202    /// Sets the centre slice, the part that stretches.
203    pub fn center_slice(mut self, slice: Rect) -> Self {
204        self.center_slice = Some(slice);
205        self
206    }
207
208    /// Sets how the picture repeats.
209    pub fn repeat(mut self, repeat: ImageRepeat) -> Self {
210        self.repeat = repeat;
211        self
212    }
213
214    /// Mirrors the picture when the box reads right to left.
215    pub fn match_text_direction(mut self, matches: bool) -> Self {
216        self.match_text_direction = matches;
217        self
218    }
219
220    /// Sets the picture's pixels per logical pixel.
221    pub fn scale(mut self, scale: f32) -> Self {
222        self.scale = scale;
223        self
224    }
225
226    /// Sets the opacity.
227    pub fn opacity(mut self, opacity: f32) -> Self {
228        self.opacity = opacity;
229        self
230    }
231
232    /// Inverts the picture's colours.
233    pub fn invert_colors(mut self, invert: bool) -> Self {
234        self.invert_colors = invert;
235        self
236    }
237
238    /// The picture `t` of the way from `a` to `b` — the reference's
239    /// `DecorationImage.lerp`. Two different pictures are not blended into a third: both
240    /// are painted, the first at `1 - t` and the second added to it at `t`. A picture on
241    /// one side only fades in or out. At `0` and `1` the answer is the end itself.
242    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        // What the transition reports as its own settings: the arriving picture's, or
254        // the leaving one's when nothing arrives — as the reference's blend does.
255        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    /// Where each part of the picture lands when it is painted into `rect`, for a box
267    /// reading in `direction`, given the picture's pixel `size` — the reference's
268    /// `paintImage`, short of the drawing. Also says whether the parts repeat, which is
269    /// when they are clipped to `rect`.
270    ///
271    /// Empty when there is nothing to draw: an empty box, an empty picture, or a fit
272    /// that leaves no room.
273    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        // The slice's border: what lies outside the centre, drawn at its own size.
287        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        // Filling the box exactly, there is nothing to repeat.
320        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        // The spare room, and where the alignment puts the picture in it. A mirrored
328        // picture is placed by the mirrored alignment, and the whole lot mirrored back
329        // below — so the picture lands where the alignment says, reversed.
330        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                // The part of the picture a crop keeps, placed by the alignment.
348                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            // Mirrored about the box's centre: each part lands on the other side, read
370            // backwards.
371            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    /// Paints the picture into `rect`, for a box reading in `direction`, at `blend` of
383    /// its opacity — the reference's `DecorationImagePainter.paint`, short of the clip,
384    /// which the decoration adds.
385    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        // The colour filter first, then the inversion over its result, as the
416        // reference's paint applies them.
417        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    /// Both pictures, the arriving one over the leaving one; one alone, faded.
428    fn paint(&self, scene: &mut Scene, rect: Rect, direction: TextDirection, blend: f32) {
429        match (&self.from, &self.to) {
430            (Some(from), Some(to)) => {
431                // The reference's sum (milestone 646): the leaving picture at `1 - t`,
432                // the arriving one **added** to it at `t`, in a group of their own so
433                // the sum is of the two pictures and not of what lies under them.
434                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
448/// Wraps what was painted since `start` into one layer, at `opacity`, through `filter`.
449fn 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
469/// The copies of `placed` that cover `rect`, along the axes `repeat` names — the
470/// reference's `_generateImageTileRects`, stopped at [`MAX_IMAGE_TILES`].
471fn 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
501/// The nine parts of a picture of pixel `size` stretched into `dst` around its centre
502/// `slice` (in the picture's units, its pixels over `scale`): the corners at their own
503/// size, the edges stretched along their length, the centre both ways. `dst` is never
504/// smaller than the corners: [`DecorationImage::pieces`] grows it past a box too small
505/// for them, as the reference does.
506fn nine(pieces: &mut Vec<ImagePiece>, size: Size, slice: Rect, scale: f32, dst: Rect) {
507    // The picture's columns and rows, in pixels, the slice clamped to it.
508    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    // The fixed parts, in logical pixels.
521    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    /// The reference's table, for a picture twice as wide as it is tall in a square.
579    #[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        // In a box wider than the picture, the width fit crops and the height fit
605        // letterboxes.
606        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        // Scaled down to the box's height, then its width.
624        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        // Never up.
633        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    /// The default: at its own size or smaller, centred, the whole picture.
650    #[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    /// A crop keeps the part the alignment names.
670    #[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(&centre, 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    /// Repeated from where the picture sits, outwards, until the box is covered, and
701    /// clipped to it.
702    #[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    /// A picture that already fills the box has nothing to repeat.
729    #[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    /// The corners keep their size, the edges stretch along their length, the centre
752    /// both ways.
753    #[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    /// Corners bigger than the box keep their size, centred on it, and the middle has no
788    /// room: the reference's, which draws past the box. A decoration clips it to its
789    /// shape.
790    #[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    /// A sliced picture repeats as a whole, each copy sliced.
806    #[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    /// At a scale of two, a picture's pixels are half a logical pixel each.
814    #[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    /// Mirrored in a right-to-left box: the picture lands where the alignment puts it,
835    /// read backwards.
836    #[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        // A start alignment is the right in a right-to-left box.
848        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    /// A mirrored slice is the whole slice mirrored: the left border lands on the right.
859    #[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        // The picture's left border, five wide, is drawn at the right edge, reversed.
867        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        // Its right border, twenty wide, at the left.
870        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    /// The colour filter, then the inversion of its result.
906    #[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    /// Two pictures: the leaving one whole, the arriving one over it at `t`. One alone
964    /// fades.
965    #[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        // The arriving picture added to the leaving one, the pair in a group of its own.
986        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}