Skip to main content

frus_widgets/
image.rs

1//! [`Image`]: displays a bitmap image ([`frus_core::ImageHandle`]), fitted according to
2//! a [`BoxFit`] — aspect preserved, letterboxed or cropped. The image is uploaded
3//! **once** then cached by the renderer, keyed on the `ImageData`'s identity.
4//!
5//! **How big is it?** Whatever you say, and failing that whatever the bitmap is. The
6//! reference's rule, and the only one that lets an image be shown by someone who does not
7//! already know its pixel dimensions:
8//!
9//! | given | box |
10//! |---|---|
11//! | width and height | that box |
12//! | one of the two | the other from the image's own ratio |
13//! | neither | the image's own size |
14
15use std::cell::OnceCell;
16
17use frus_core::{
18    Alignment, AlignmentGeometry, BoxFit, Color, Curve, DecodeSize, ImageHandle, Rect, Scene,
19};
20use frus_layout::{Dimension, Style};
21
22use crate::interaction::Status;
23use crate::skeleton::Skeleton;
24use crate::theme::Theme;
25use crate::widget::Widget;
26
27/// Turns file bytes into pixels at `size`, through whichever decoder this build carries —
28/// and says so in the console when it cannot. [`frus_core::cached_at`] and
29/// [`frus_core::fetched_at`] call this once per image and size, so it is said once; an
30/// image that paints nothing with no word anywhere is otherwise found on a device
31/// (milestone 626).
32fn decode_at(bytes: &[u8], size: DecodeSize) -> Result<frus_core::ImageData, String> {
33    decode_in_this_build(bytes, size).inspect_err(|why| {
34        log::warn!(
35            "frus: an image of {} bytes could not be decoded: {why}",
36            bytes.len()
37        )
38    })
39}
40
41#[cfg(feature = "images")]
42fn decode_in_this_build(bytes: &[u8], size: DecodeSize) -> Result<frus_core::ImageData, String> {
43    frus_image::decode_at(bytes, size).map_err(|e| e.message().to_string())
44}
45
46/// Without the `images` feature there is no decoder, and saying so is the whole job.
47/// It must not panic: an application that dropped the feature deliberately, or a test
48/// binary built with `--no-default-features`, is not a broken program. The message says
49/// how to get it back, because the usual way to lose it is `default-features = false`,
50/// written to drop the bundled fonts.
51#[cfg(not(feature = "images"))]
52fn decode_in_this_build(_bytes: &[u8], _size: DecodeSize) -> Result<frus_core::ImageData, String> {
53    Err(
54        "no image decoder: this build dropped the `images` feature, which \
55         `default-features = false` turns off; add `features = [\"images\"]` to the frus \
56         dependency"
57            .to_string(),
58    )
59}
60
61/// Where an [`Image`]'s pixels are: here, on their way, or never coming.
62///
63/// Three and not two, because *not here yet* and *will never be here* are different
64/// answers and an interface shows them differently — one is a placeholder, the other is
65/// a message.
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub enum State<'a> {
68    /// The pixels are here.
69    Ready,
70    /// In flight, or being decoded; a later frame will have them. [`Image::network`]
71    /// reports this while it fetches, and [`Image::memory`] while a large picture decodes.
72    Loading,
73    /// They will not arrive, and this is why.
74    Failed(&'a str),
75}
76
77/// The same three, owning what they carry.
78enum Source {
79    Ready(ImageHandle),
80    Loading,
81    Failed(String),
82}
83
84/// The most pixels an embedded picture can have and still be decoded in the frame that
85/// shows it: 256 × 256. Above it, [`Image::memory`] decodes on a background thread.
86pub const QUICK_DECODE_PIXELS: u64 = 256 * 256;
87
88/// Whether `bytes` hold a picture small enough to decode in the frame, from its header. One
89/// whose header cannot be read is decoded at once, which is where its failure is reported.
90#[cfg(feature = "images")]
91fn decodes_quickly(bytes: &[u8]) -> bool {
92    frus_image::dimensions(bytes)
93        .is_none_or(|(w, h)| u64::from(w) * u64::from(h) <= QUICK_DECODE_PIXELS)
94}
95
96/// Without a decoder there is nothing to put on a thread.
97#[cfg(not(feature = "images"))]
98fn decodes_quickly(_bytes: &[u8]) -> bool {
99    true
100}
101
102/// Where the bytes of an [`Image`] that has not been resolved yet are.
103enum Origin {
104    Memory(&'static [u8]),
105    Network(String),
106}
107
108/// Physical pixels are asked for in steps of this many, so a picture whose box animates
109/// is decoded a handful of times rather than once per frame of the animation.
110const DECODE_STEP: u32 = 32;
111
112/// How long an image takes to cross over its placeholder — the reference's own figure
113/// for `FadeInImage` (`fade_in_image.dart:37`).
114pub const IMAGE_FADE: f32 = 0.7;
115
116/// **What stands in for an image while it is on its way.**
117///
118/// Not [`crate::Placeholder`], which is the design tool's crossed box: this one is the
119/// slot [`Image::placeholder`] fills.
120///
121/// Three, because they are the three things anybody puts there: a flat colour, the
122/// shimmering block a list of thumbnails wants, or a smaller picture — a blurred
123/// thumbnail, a low-resolution copy, the one already in the cache.
124///
125/// It is **not a widget slot**, and that is deliberate: [`Image`] carries no message type
126/// (`impl<Msg> Widget<Msg> for Image`), which is what lets one be built once and dropped
127/// into any tree. Taking an arbitrary child would make it `Image<Msg>` and cost that
128/// everywhere, to buy a fourth case nobody has asked for.
129pub enum ImagePlaceholder {
130    /// A flat colour.
131    Color(Color),
132    /// The shimmering block, at whatever shape it was built with.
133    Skeleton(Skeleton),
134    /// Another picture: a blur, a thumbnail, the cached copy.
135    Image(Box<Image>),
136}
137
138impl From<Color> for ImagePlaceholder {
139    fn from(color: Color) -> Self {
140        ImagePlaceholder::Color(color)
141    }
142}
143
144impl From<Skeleton> for ImagePlaceholder {
145    fn from(skeleton: Skeleton) -> Self {
146        ImagePlaceholder::Skeleton(skeleton)
147    }
148}
149
150impl From<Image> for ImagePlaceholder {
151    fn from(image: Image) -> Self {
152        ImagePlaceholder::Image(Box::new(image))
153    }
154}
155
156impl ImagePlaceholder {
157    /// Draws the stand-in into `bounds` at `opacity`.
158    fn paint(&self, bounds: Rect, status: &Status, theme: &Theme, scene: &mut Scene) {
159        match self {
160            ImagePlaceholder::Color(color) => scene.fill_rect(bounds, color.fade(status.opacity)),
161            // Both of these are widgets already, and both are free of a message type, so
162            // they are painted where they stand rather than mounted as children. A child
163            // would have to be laid out, and the whole of what a placeholder wants is the
164            // box the image was going to have.
165            ImagePlaceholder::Skeleton(skeleton) => {
166                Widget::<()>::paint(skeleton, bounds, *status, theme, scene)
167            }
168            ImagePlaceholder::Image(image) => {
169                Widget::<()>::paint(image.as_ref(), bounds, *status, theme, scene)
170            }
171        }
172    }
173}
174
175/// An image, fitted by `fit` into whatever box it ends up with.
176pub struct Image {
177    /// Where the bytes are, for an image decoded by this widget; `None` for one built from
178    /// pixels the application already had.
179    origin: Option<Origin>,
180    /// The pixels, why there are none, or that they are still on their way — resolved
181    /// from `origin` the first time anybody asks, once the size it is decoded at is known.
182    source: OnceCell<Source>,
183    /// The size asked for with [`Image::cache_width`] and [`Image::cache_height`].
184    cache: DecodeSize,
185    /// Whether [`Image::full_resolution`] turned decoding at the shown size off.
186    full_resolution: bool,
187    width: Option<f32>,
188    height: Option<f32>,
189    fit: BoxFit,
190    tint: Option<Color>,
191    opacity: f32,
192    alignment: AlignmentGeometry,
193    /// What a screen reader is told this picture is; see [`Image::semantic_label`].
194    semantic_label: Option<String>,
195    /// Whether to leave it out of the tree a screen reader walks.
196    exclude_from_semantics: bool,
197    /// Whether the picture is **mirrored** in a right-to-left reading direction.
198    match_text_direction: bool,
199    /// What stands in while the pixels are on their way; `None` leaves the box empty,
200    /// which is what an image has always done.
201    placeholder: Option<ImagePlaceholder>,
202    /// How long the picture takes to cross over the placeholder.
203    fade: f32,
204    fade_curve: Curve,
205}
206
207impl Image {
208    /// An image at **its own size**, fitted with [`BoxFit::Contain`] by default.
209    pub fn new(image: ImageHandle) -> Self {
210        Self::from_source(Source::Ready(image))
211    }
212
213    /// An image **decoded from embedded bytes** — what `include_bytes!` gives, and the
214    /// answer to "how do I just show my logo".
215    ///
216    /// ```ignore
217    /// Image::memory(include_bytes!("../assets/logo.png")).width(96.0)
218    /// ```
219    ///
220    /// The bytes are decoded **once per process**, not once per frame. A view is rebuilt
221    /// every frame and decoding a PNG is not free, so the result is kept in a shared
222    /// store keyed by where the bytes live — see [`frus_core::cached`] for why an
223    /// address is the right key and why the `'static` bound is what makes it sound.
224    ///
225    /// **Failure paints nothing.** A file that is not an image does not become one on the
226    /// next frame, so the failure is remembered rather than retried, and the widget
227    /// occupies whatever box it was given without drawing into it. That is the
228    /// reference's behaviour for an image with no error widget supplied, and
229    /// [`Image::error`] is how an application asks what went wrong.
230    ///
231    /// **Decoded at the size shown**: see [`Image::cache_width`].
232    ///
233    /// **A large picture is decoded on a background thread**, so the frame that first
234    /// shows it does not stop for the tens of milliseconds a photograph takes: until it is
235    /// there the image is [`State::Loading`] and shows its
236    /// [`placeholder`](Image::placeholder), as a network image does, and the frames keep
237    /// coming. One of at most [`QUICK_DECODE_PIXELS`] — an icon, a logo, an avatar — is
238    /// decoded at once, since it takes less time than the thread would to pick it up and
239    /// a blink as it came in would be the only difference. The Web, which has no threads,
240    /// decodes every picture at once.
241    ///
242    /// Needs the `images` feature, which is on by default. Without it this reports a
243    /// missing decoder rather than panicking — the same rule the bundled fonts follow.
244    pub fn memory(bytes: &'static [u8]) -> Self {
245        Self::from_origin(Origin::Memory(bytes))
246    }
247
248    /// An image **fetched over the network**, decoded and shown when it arrives.
249    ///
250    /// ```ignore
251    /// Image::network("https://example.com/ada.png").width(240.0)
252    /// ```
253    ///
254    /// The **first** frame that asks starts the request and reports [`State::Loading`];
255    /// every later frame is a lookup. That is what makes this safe to write in a view,
256    /// which runs sixty times a second — a fetch per frame would be sixty requests for
257    /// one picture. The interface keeps drawing while anything is in flight, so the
258    /// frame that shows the picture does happen without the application arranging it.
259    ///
260    /// There is no `loading` or `error` **slot** here, and that is the framework's model
261    /// rather than a gap. A view is a function of state, and this state is readable:
262    /// [`Image::state`] answers, and the application writes the `match` it would have
263    /// written anyway.
264    ///
265    /// ```ignore
266    /// let photo = Image::network(&url).width(240.0);
267    /// match photo.state() {
268    ///     State::Loading => CircularProgressIndicator::new().boxed(),
269    ///     State::Failed(why) => text(why).boxed(),
270    ///     State::Ready => photo.boxed(),
271    /// }
272    /// ```
273    ///
274    /// Size it **before** asking: the size is what it is fetched and decoded at, and one
275    /// set after the question is a second request at the new size.
276    ///
277    /// A closure stored in the widget would say the same thing in a place the
278    /// application cannot see it, and would need the widget to carry the message type
279    /// for the sake of a branch the view can already take.
280    ///
281    /// **The request starts the first time the image is asked about** — laid out,
282    /// painted, or asked its [`state`](Image::state) — since that is when the size to
283    /// decode it at is known (see [`Image::cache_width`]). An image built and dropped
284    /// without any of those fetches nothing.
285    ///
286    /// Needs a registered fetcher — [`frus_core::set_image_fetcher`] — which the shell
287    /// installs on the way up when built with the `net` feature. Without one this fails
288    /// with a message saying so, rather than waiting for ever.
289    pub fn network(url: impl AsRef<str>) -> Self {
290        Self::from_origin(Origin::Network(url.as_ref().to_string()))
291    }
292
293    /// The pixels, resolving them on the first ask.
294    fn source(&self) -> &Source {
295        self.source.get_or_init(|| {
296            let size = self.decode_size();
297            match &self.origin {
298                Some(Origin::Memory(bytes)) => {
299                    let later = !decodes_quickly(bytes);
300                    match frus_core::decoded_at(bytes, size, decode_at, later) {
301                        frus_core::Fetched::Loading => Source::Loading,
302                        frus_core::Fetched::Ready(handle) => Source::Ready(handle),
303                        frus_core::Fetched::Failed(why) => Source::Failed(why),
304                    }
305                }
306                Some(Origin::Network(url)) => match frus_core::fetched_at(url, size, decode_at) {
307                    frus_core::Fetched::Loading => Source::Loading,
308                    frus_core::Fetched::Ready(handle) => Source::Ready(handle),
309                    frus_core::Fetched::Failed(why) => Source::Failed(why),
310                },
311                // Built from pixels, and those were put in the cell at construction.
312                None => Source::Failed("no image".to_string()),
313            }
314        })
315    }
316
317    /// The size the bytes are decoded at: what [`Image::cache_width`] and
318    /// [`Image::cache_height`] said, or failing that the size of the box, when the
319    /// application gave one, in physical pixels.
320    fn decode_size(&self) -> DecodeSize {
321        if !self.cache.is_full() {
322            return self.cache;
323        }
324        // `BoxFit::None` paints the picture at its own size, which a smaller decode would
325        // change.
326        if self.full_resolution || self.fit == BoxFit::None {
327            return DecodeSize::FULL;
328        }
329        let ratio = crate::MediaQuery::of().device_pixel_ratio;
330        let ratio = if ratio.is_finite() && ratio > 0.0 {
331            ratio
332        } else {
333            1.0
334        };
335        let physical = |logical: Option<f32>| {
336            let logical = logical.filter(|l| l.is_finite() && *l > 0.0)?;
337            let pixels = (logical * ratio).ceil() as u32;
338            Some(pixels.div_ceil(DECODE_STEP).max(1) * DECODE_STEP)
339        };
340        DecodeSize::new(physical(self.width), physical(self.height))
341    }
342
343    /// Where this image has got to: here, on its way, or never coming.
344    ///
345    /// An embedded image is [`State::Loading`] only while a large one decodes on its
346    /// thread; [`Image::network`] is loading until its bytes are here and decoded.
347    pub fn state(&self) -> State<'_> {
348        match self.source() {
349            Source::Ready(_) => State::Ready,
350            Source::Loading => State::Loading,
351            Source::Failed(why) => State::Failed(why),
352        }
353    }
354
355    /// Why there are no pixels, if there are none.
356    ///
357    /// An application that wants to put something else in the gap — a placeholder, a
358    /// message, a retry — asks here and builds it itself. That is a `match` in the view
359    /// rather than a closure the widget stores, which is the honest shape while the
360    /// pixels are something the application already holds.
361    ///
362    /// An image still **loading** has no error: it has not failed, it has not arrived.
363    /// [`Image::state`] is the one that tells the three apart.
364    pub fn error(&self) -> Option<&str> {
365        match self.source() {
366            Source::Failed(why) => Some(why),
367            _ => None,
368        }
369    }
370
371    /// The decoded pixels, once they are here: `None` while they are on their way or when
372    /// they will never come. This is how a picture loaded by `Image::memory` or
373    /// `Image::network` is painted by something other than an `Image` — a decoration, an
374    /// avatar — with the same loading and the same cache (milestone 647).
375    pub fn handle(&self) -> Option<frus_core::ImageHandle> {
376        match self.source() {
377            Source::Ready(handle) => Some(handle.clone()),
378            _ => None,
379        }
380    }
381
382    fn from_source(source: Source) -> Self {
383        let mut image = Self::blank(None);
384        image.source = OnceCell::from(source);
385        image
386    }
387
388    fn from_origin(origin: Origin) -> Self {
389        Self::blank(Some(origin))
390    }
391
392    fn blank(origin: Option<Origin>) -> Self {
393        Self {
394            origin,
395            source: OnceCell::new(),
396            cache: DecodeSize::FULL,
397            full_resolution: false,
398            width: None,
399            height: None,
400            fit: BoxFit::Contain,
401            tint: None,
402            opacity: 1.0,
403            alignment: AlignmentGeometry::Physical(Alignment::CENTER),
404            semantic_label: None,
405            exclude_from_semantics: false,
406            match_text_direction: false,
407            placeholder: None,
408            fade: IMAGE_FADE,
409            fade_curve: Curve::ease_out(),
410        }
411    }
412
413    /// Sets the width, in logical pixels. With no height, the height follows from the
414    /// image's own ratio.
415    pub fn width(mut self, width: f32) -> Self {
416        self.width = Some(width);
417        self.unresolved()
418    }
419
420    /// Sets the height, in logical pixels. With no width, the width follows from the
421    /// image's own ratio.
422    pub fn height(mut self, height: f32) -> Self {
423        self.height = Some(height);
424        self.unresolved()
425    }
426
427    /// Forgets pixels resolved before the size they are decoded at was changed, so the
428    /// next ask resolves them at the new one. An image built from pixels keeps them.
429    fn unresolved(mut self) -> Self {
430        if self.origin.is_some() {
431            self.source = OnceCell::new();
432        }
433        self
434    }
435
436    /// Both at once.
437    pub fn size(self, width: f32, height: f32) -> Self {
438        self.width(width).height(height)
439    }
440
441    /// **Decodes the picture this many physical pixels wide**, whatever the file's own
442    /// width; the height follows from the image's own ratio unless
443    /// [`cache_height`](Image::cache_height) is given too, in which case the picture is
444    /// decoded at the smallest size, at its own ratio, that covers both. Never larger
445    /// than the file.
446    ///
447    /// A twelve-megapixel photograph shown as a thumbnail does not need 48 MB of pixels,
448    /// and on a phone a grid of them is how an application is killed for memory.
449    ///
450    /// **Without it, an image given a [`width`](Image::width) or a
451    /// [`height`](Image::height) is already decoded at that size** times the screen's
452    /// pixel ratio, rounded up a little; this is for one that is not — sized by its parent
453    /// — or one that wants a different size. An image given neither, and none of these,
454    /// is decoded at the file's own size, which is also its own size on screen: setting
455    /// this on one changes the size it takes.
456    ///
457    /// Only an image this widget decodes — [`Image::memory`], [`Image::network`] — is
458    /// affected. One made with [`Image::new`] is the pixels the application gave it.
459    #[must_use]
460    pub fn cache_width(mut self, pixels: u32) -> Self {
461        self.cache.width = Some(pixels.max(1));
462        self.unresolved()
463    }
464
465    /// [`cache_width`](Image::cache_width), for the height.
466    #[must_use]
467    pub fn cache_height(mut self, pixels: u32) -> Self {
468        self.cache.height = Some(pixels.max(1));
469        self.unresolved()
470    }
471
472    /// **Decodes the file at its own size**, however small the box: for a picture that is
473    /// shown larger than its box says — under a zoom, a scale transform, a hero flight to
474    /// a full-screen view — and must stay sharp there.
475    #[must_use]
476    pub fn full_resolution(mut self) -> Self {
477        self.full_resolution = true;
478        self.unresolved()
479    }
480
481    /// Changes the fit mode.
482    pub fn fit(mut self, fit: BoxFit) -> Self {
483        self.fit = fit;
484        self.unresolved()
485    }
486
487    /// Where the image sits in a box it does not fill, and which part of it survives a
488    /// box it overflows. Centre by default, as in the reference.
489    ///
490    /// It is the same anchor either way round: aligning to the top means the top of the
491    /// box for a letterboxed image and the top of the *image* for a cropped one, which
492    /// is the answer a photograph of a person wants in both cases.
493    pub fn alignment(mut self, alignment: impl Into<AlignmentGeometry>) -> Self {
494        self.alignment = alignment.into();
495        self
496    }
497
498    /// A multiplied tint, white leaving it unchanged — for bitmap icons, say.
499    pub fn tint(mut self, tint: Color) -> Self {
500        self.tint = Some(tint);
501        self
502    }
503
504    /// Draws the image at a fraction of its opacity, `1.0` being fully opaque.
505    ///
506    /// Unlike a group opacity this needs no layer: an image is one primitive, so the
507    /// fade goes into the tint it is already multiplied by.
508    pub fn opacity(mut self, opacity: f32) -> Self {
509        self.opacity = opacity.clamp(0.0, 1.0);
510        self
511    }
512
513    /// What a screen reader says instead of the picture.
514    ///
515    /// A picture with no label is a picture nobody reading by ear can see, and until
516    /// this existed **every** image in every application was one: the vocabulary had
517    /// [`frus_core::Role::Image`] in it and nothing ever emitted it.
518    ///
519    /// Say what the picture *is*, not that it is a picture — a reader is already told
520    /// the role. If it carries no meaning of its own, do not label it: reach for
521    /// [`exclude_from_semantics`](Image::exclude_from_semantics) instead.
522    pub fn semantic_label(mut self, label: impl Into<String>) -> Self {
523        self.semantic_label = Some(label.into());
524        self
525    }
526
527    /// Leaves the image out of the tree a screen reader walks.
528    ///
529    /// The right answer for **decoration** — a divider, a texture, a shape behind a
530    /// heading. Announcing those interrupts a reader with something that was never
531    /// meant to be read, and an empty label would still announce the role. This is the
532    /// reference's `excludeFromSemantics`, and like it, it wins over any label given.
533    pub fn exclude_from_semantics(mut self, exclude: bool) -> Self {
534        self.exclude_from_semantics = exclude;
535        self
536    }
537
538    /// Mirrors the image horizontally when the reading direction is right-to-left.
539    ///
540    /// **Off** by default, which is the reference's default and the right one: an image
541    /// is a picture rather than a run of text, and a photograph of a person does not
542    /// want to be flipped because the interface is in Arabic. It is a per-image
543    /// decision, so it is a per-image switch.
544    ///
545    /// Turn it on for a picture that **points**: an arrow meaning *forward*, a
546    /// speech bubble with a tail, a hand indicating the next step. Those follow the
547    /// direction the reader's eye travels, and in RTL that is the other way round.
548    pub fn match_text_direction(mut self, match_direction: bool) -> Self {
549        self.match_text_direction = match_direction;
550        self
551    }
552
553    /// **What stands in while the pixels are on their way**, and what the picture crosses
554    /// over when they arrive.
555    ///
556    /// Takes a colour, a [`Skeleton`], or another [`Image`] — see [`ImagePlaceholder`]. Without
557    /// one the box stays empty and the picture appears in a single frame, which is what an
558    /// image has always done here and what a list of thumbnails should not do.
559    ///
560    /// ```ignore
561    /// Image::network(url).width(120.0).placeholder(Skeleton::new().height(90.0))
562    /// ```
563    #[must_use]
564    pub fn placeholder(mut self, placeholder: impl Into<ImagePlaceholder>) -> Self {
565        self.placeholder = Some(placeholder.into());
566        self
567    }
568
569    /// How long the crossing takes, in seconds. [`IMAGE_FADE`] — the reference's 700 ms —
570    /// unless a caller says otherwise. `0.0` is a hard cut with a placeholder before it.
571    #[must_use]
572    pub fn fade_duration(mut self, seconds: f32) -> Self {
573        self.fade = seconds.max(0.0);
574        self
575    }
576
577    /// The crossing's curve; [`Curve::ease_out`](Curve::ease_out) by
578    /// default, so the picture arrives quickly and settles.
579    #[must_use]
580    pub fn fade_curve(mut self, curve: Curve) -> Self {
581        self.fade_curve = curve;
582        self
583    }
584
585    /// Whether the picture is here.
586    fn ready(&self) -> bool {
587        matches!(self.source(), Source::Ready(_))
588    }
589
590    /// The box this asks for, given the bitmap's own size.
591    fn box_style(&self) -> Style {
592        // Nothing decoded: there is no natural size to fall back on, so an image given
593        // no measurements takes no room at all. Anything the caller *did* say is still
594        // honoured, which is what keeps a layout from jumping when one asset is broken.
595        let Source::Ready(image) = self.source() else {
596            return Style {
597                width: self.width.map_or(Dimension::Length(0.0), Dimension::Length),
598                height: self
599                    .height
600                    .map_or(Dimension::Length(0.0), Dimension::Length),
601                ..Default::default()
602            };
603        };
604        let natural = image.size();
605        let ratio = if natural.height > 0.0 {
606            natural.width / natural.height
607        } else {
608            1.0
609        };
610        match (self.width, self.height) {
611            (Some(w), Some(h)) => Style {
612                width: Dimension::Length(w),
613                height: Dimension::Length(h),
614                ..Default::default()
615            },
616            // One side and a ratio: the layout engine derives the other, which is the
617            // reference's rule and the reason an image can be given a width alone.
618            (Some(w), None) => Style {
619                width: Dimension::Length(w),
620                aspect_ratio: Some(ratio),
621                ..Default::default()
622            },
623            (None, Some(h)) => Style {
624                height: Dimension::Length(h),
625                aspect_ratio: Some(ratio),
626                ..Default::default()
627            },
628            (None, None) => Style {
629                width: Dimension::Length(natural.width),
630                height: Dimension::Length(natural.height),
631                ..Default::default()
632            },
633        }
634    }
635}
636
637impl<Msg> Widget<Msg> for Image {
638    fn style(&self) -> Style {
639        self.box_style()
640    }
641
642    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
643        &[]
644    }
645
646    fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
647        self.paint_tinted(bounds, status, theme, scene, None);
648    }
649
650    /// **Where the crossing is going**: all the way over once the pixels are here, and
651    /// nowhere at all until then.
652    ///
653    /// Only claimed when there is a placeholder, so an image without one costs the runtime
654    /// nothing and keeps the behaviour it has always had.
655    fn anim_target(&self) -> Option<f32> {
656        self.placeholder
657            .as_ref()
658            .map(|_| if self.ready() { 1.0 } else { 0.0 })
659    }
660
661    /// **The crossing has a duration in one direction only.**
662    ///
663    /// Coming in it takes its time: that is the whole point. Going back — a source
664    /// replaced at the same place in the tree, so the widget is loading again — it is
665    /// instant, because there is nothing left to cross *from*. The pixels of the picture
666    /// that was there are already gone by the time this is asked; tweening down would fade
667    /// the placeholder **in** over three quarters of a second while nothing else was
668    /// drawn, which reads as a stall rather than as a change.
669    ///
670    /// This is also what answers *do not restart on a rebuild*. The value lives against
671    /// the widget's place in the tree and survives every rebuild, so nothing restarts
672    /// while the source stands still; and a source that changes passes through *not
673    /// ready*, which resets it to nought in one frame and lets the next arrival cross
674    /// properly. No record of which image this timeline is about is needed, because the
675    /// journey through the middle is the record.
676    fn anim_duration(&self) -> f32 {
677        match self.ready() {
678            true => self.fade,
679            false => 0.0,
680        }
681    }
682
683    fn anim_curve(&self) -> Curve {
684        self.fade_curve.clone()
685    }
686
687    fn on_click(&self) -> Option<Msg> {
688        None
689    }
690
691    fn semantics(&self) -> Option<frus_core::SemanticsProperties> {
692        if self.exclude_from_semantics {
693            return None;
694        }
695        let mut semantics = frus_core::SemanticsProperties::new(frus_core::Role::Image);
696        if let Some(label) = self.semantic_label.as_deref() {
697            semantics = semantics.label(label);
698        }
699        Some(semantics)
700    }
701}
702
703impl Image {
704    /// The paint, with a **tint the caller supplies** rather than the one on the widget —
705    /// what [`ImageIcon`] needs, since it holds an `Image` by reference and cannot rebuild
706    /// one to colour it.
707    pub(crate) fn paint_tinted(
708        &self,
709        bounds: Rect,
710        status: Status,
711        theme: &Theme,
712        scene: &mut Scene,
713        over: Option<Color>,
714    ) {
715        // **How far across the crossing is**, and it is only asked for when there is
716        // something to cross over from. Without a placeholder the picture is drawn at its
717        // own opacity as it always was, so nothing already written changes.
718        let across = match self.placeholder {
719            Some(_) => status.value.clamp(0.0, 1.0),
720            None => 1.0,
721        };
722        if let Some(placeholder) = &self.placeholder {
723            if across < 1.0 {
724                let under = Status {
725                    opacity: status.opacity * (1.0 - across),
726                    ..status
727                };
728                placeholder.paint(bounds, &under, theme, scene);
729            }
730        }
731        // The **alignment** stays physical whatever the direction. An image is a
732        // picture rather than a run of text: a portrait aligned to the top of its
733        // crop wants the top of its crop in every language. Mirroring is the separate,
734        // opt-in question below, which is how the reference splits it too.
735        // Nothing decoded, nothing drawn: the box stays empty rather than showing a
736        // stand-in nobody asked for. `Image::error` is how an application finds out.
737        let Source::Ready(image) = self.source() else {
738            return;
739        };
740        let align = self.alignment.resolve(frus_core::TextDirection::Ltr);
741        let (dst, mut uv) = self.fit.apply_aligned(image.size(), bounds, align);
742        // A mirror costs nothing but a sign. The shader reads
743        // `uv.xy + unit_pos * uv.zw` with `unit_pos` running 0..1, so a **negative**
744        // width walks the same span backwards — which is the reference's "scaling
745        // factor of -1 in the horizontal direction", without a transform, a layer, or
746        // a second copy of the pixels.
747        if self.match_text_direction && theme.direction == frus_core::TextDirection::Rtl {
748            uv = Rect::new(uv.x + uv.width, uv.y, -uv.width, uv.height);
749        }
750        let tint = over
751            .or(self.tint)
752            .unwrap_or(Color::WHITE)
753            .fade(status.opacity * self.opacity * across);
754        scene.draw_image(image, dst, uv, tint);
755    }
756}
757
758/// **A picture used where an icon would go**: a brand mark, a flag, a custom glyph that is
759/// artwork rather than a path.
760///
761/// It is an icon in every way that matters to the layout and the theme — the ambient size
762/// from [`IconTheme`](crate::IconTheme), the same square box, the same place in a row of
763/// them — and a picture in the one way that matters to the painter. So it sits in an
764/// [`IconButton`](crate::IconButton), a list tile's leading slot or an app bar action
765/// without any of them knowing.
766///
767/// **Untinted by default**, which is the one place it parts from [`crate::Icon`]. An icon
768/// is a silhouette and takes the foreground colour; a picture usually has colours of its
769/// own, and a brand mark flattened to `on_surface` the first time an application themes its
770/// icons is a brand mark nobody recognises. [`ImageIcon::color`] is for the case that wants
771/// it — a monochrome glyph shipped as a bitmap, which is why the reference's own honours
772/// `IconTheme.color` at all.
773///
774/// ```ignore
775/// ImageIcon::new(Image::memory(include_bytes!("../assets/mark.png")))
776/// ```
777pub struct ImageIcon {
778    image: Image,
779    size: Option<f32>,
780    color: Option<Color>,
781}
782
783impl ImageIcon {
784    /// A picture at the ambient icon size, in its own colours.
785    pub fn new(image: Image) -> Self {
786        Self {
787            image,
788            size: None,
789            color: None,
790        }
791    }
792
793    /// The square's side, in logical pixels. Outranks the theme.
794    #[must_use]
795    pub fn size(mut self, size: f32) -> Self {
796        self.size = Some(size);
797        self
798    }
799
800    /// **Tints** the picture, the way an icon is coloured.
801    ///
802    /// Multiplied into the pixels, so it is only a colour for artwork that is white or
803    /// grey to begin with — which is what a bitmap glyph is. A photograph tinted red comes
804    /// out a red photograph.
805    ///
806    /// Say it with [`crate::Icon`]'s own default and the picture follows the icon theme:
807    /// `image_icon.color(theme.widgets.icon.color.unwrap_or(theme.on_surface))`. It is not
808    /// done for you, for the reason in the type's own documentation.
809    #[must_use]
810    pub fn color(mut self, color: Color) -> Self {
811        self.color = Some(color);
812        self
813    }
814
815    /// The side actually drawn: `caller ?? theme ?? the grid`. The same chain
816    /// [`crate::Icon`] answers, so a picture and a path line up in one row.
817    fn resolved_size(&self, theme: Option<&Theme>) -> f32 {
818        self.size
819            .or_else(|| theme.and_then(|t| t.widgets.icon.size))
820            .unwrap_or(crate::icons::GRID)
821    }
822
823    fn sized(&self, side: f32) -> Style {
824        Style {
825            width: Dimension::Length(side),
826            height: Dimension::Length(side),
827            ..Default::default()
828        }
829    }
830
831    /// Draws the picture into a square somebody else chose — what
832    /// [`IconButton`](crate::IconButton) needs, since a button sizes its own mark and a
833    /// picture must be the same size as the path beside it.
834    pub(crate) fn paint_at(&self, square: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
835        self.image
836            .paint_tinted(square, status, theme, scene, self.color);
837    }
838}
839
840impl<Msg> Widget<Msg> for ImageIcon {
841    fn style(&self) -> Style {
842        self.sized(self.resolved_size(None))
843    }
844
845    /// The theme has a say in the **size**, not only the colour, exactly as it does for a
846    /// path icon — so an app bar that makes its glyphs smaller makes this smaller too.
847    fn style_themed(&self, theme: &Theme) -> Style {
848        self.sized(self.resolved_size(Some(theme)))
849    }
850
851    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
852        &[]
853    }
854
855    fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
856        // The square the icon would have had, centred in whatever box the layout gave —
857        // the same two lines `Icon` uses, which is what keeps a picture and a path level
858        // in one row.
859        let side = self.resolved_size(Some(theme));
860        let square = Rect::new(
861            bounds.x + (bounds.width - side) * 0.5,
862            bounds.y + (bounds.height - side) * 0.5,
863            side,
864            side,
865        );
866        self.paint_at(square, status, theme, scene);
867    }
868
869    fn on_click(&self) -> Option<Msg> {
870        None
871    }
872
873    /// The crossing is the picture's, forwarded — a brand mark fetched over the network
874    /// fades in behind an icon button exactly as it would anywhere else.
875    fn anim_target(&self) -> Option<f32> {
876        Widget::<()>::anim_target(&self.image)
877    }
878
879    fn anim_duration(&self) -> f32 {
880        Widget::<()>::anim_duration(&self.image)
881    }
882
883    fn anim_curve(&self) -> Curve {
884        Widget::<()>::anim_curve(&self.image)
885    }
886
887    fn semantics(&self) -> Option<frus_core::SemanticsProperties> {
888        Widget::<()>::semantics(&self.image)
889    }
890}
891
892#[cfg(test)]
893mod tests {
894    use super::*;
895    use crate::Runtime;
896    use frus_core::{ImageData, Primitive};
897
898    fn handle(w: u32, h: u32) -> ImageHandle {
899        ImageData::from_rgba(w, h, vec![255u8; (w * h * 4) as usize]).into_handle()
900    }
901
902    /// Renders one image on its own and returns `(the image tints, the flat fills)`.
903    fn painted(image: Image, value: f32) -> (Vec<Color>, Vec<Color>) {
904        let mut runtime = Runtime::default();
905        runtime.set_value(crate::interaction::WidgetId::ROOT, value);
906        let ui: crate::Ui<()> = crate::ui::build_ui(
907            &image,
908            frus_core::Size::new(100.0, 100.0),
909            &runtime,
910            &Theme::default(),
911        );
912        let mut images = Vec::new();
913        let mut fills = Vec::new();
914        fn walk(primitives: &[Primitive], images: &mut Vec<Color>, fills: &mut Vec<Color>) {
915            for p in primitives {
916                match p {
917                    Primitive::Image { tint, .. } => images.push(*tint),
918                    Primitive::Rect { color, .. } => fills.push(*color),
919                    Primitive::Layer { primitives, .. } => walk(primitives, images, fills),
920                    _ => {}
921                }
922            }
923        }
924        walk(ui.scene().primitives(), &mut images, &mut fills);
925        (images, fills)
926    }
927
928    /// **An image with no placeholder is drawn exactly as it always was.** The crossing is
929    /// only asked for when there is something to cross over from, so nothing already
930    /// written acquired an animation or a frame of transparency.
931    #[test]
932    fn an_image_with_no_placeholder_is_untouched() {
933        let image = Image::new(handle(8, 8)).size(50.0, 50.0);
934        assert_eq!(Widget::<()>::anim_target(&image), None);
935        // Even asked at nought — which is what a runtime that had never seen it would
936        // answer — the picture is opaque.
937        let (images, _) = painted(image, 0.0);
938        assert_eq!(images.len(), 1);
939        assert_eq!(images[0].a, 1.0);
940    }
941
942    /// **Half way across, both are drawn and neither is whole.** That is the crossing: the
943    /// placeholder is on its way out at the same rate the picture is on its way in, so the
944    /// box is never empty and never shows two solid things at once.
945    #[test]
946    fn half_way_across_both_are_drawn() {
947        let stand_in = Color::rgb(0.9, 0.1, 0.1);
948        let image = Image::new(handle(8, 8))
949            .size(50.0, 50.0)
950            .placeholder(stand_in);
951        let (images, fills) = painted(image, 0.5);
952        assert_eq!(images.len(), 1, "the picture");
953        assert!(
954            (images[0].a - 0.5).abs() < 1e-3,
955            "half faded in: {:?}",
956            images[0].a
957        );
958        let under = fills
959            .iter()
960            .find(|c| (c.r, c.g, c.b) == (stand_in.r, stand_in.g, stand_in.b))
961            .expect("the placeholder");
962        assert!(
963            (under.a - 0.5).abs() < 1e-3,
964            "half faded out: {:?}",
965            under.a
966        );
967    }
968
969    /// **At rest the placeholder is not drawn at all** — not drawn at nought opacity,
970    /// which would still be a primitive per image on every frame of a list of thumbnails.
971    #[test]
972    fn at_rest_only_the_picture_is_drawn() {
973        let stand_in = Color::rgb(0.9, 0.1, 0.1);
974        let image = Image::new(handle(8, 8))
975            .size(50.0, 50.0)
976            .placeholder(stand_in);
977        let (images, fills) = painted(image, 1.0);
978        assert_eq!(images.len(), 1);
979        assert_eq!(images[0].a, 1.0);
980        assert!(
981            !fills
982                .iter()
983                .any(|c| (c.r, c.g, c.b) == (stand_in.r, stand_in.g, stand_in.b)),
984            "no placeholder once it is over"
985        );
986    }
987
988    /// **The crossing has a duration in one direction only.**
989    ///
990    /// Coming in it takes its time. Going back it is instant, because there is nothing
991    /// left to cross *from*: the pixels of the picture that was there are gone by then, so
992    /// a tween down would fade the placeholder **in** over three quarters of a second with
993    /// nothing else on screen — a stall rather than a change.
994    ///
995    /// It is also what makes a source swapped at one place in the tree do the right thing
996    /// without anyone recording which picture the timeline was about: the swap passes
997    /// through *not ready*, which resets it in one frame, and the next arrival crosses
998    /// properly.
999    #[test]
1000    fn the_crossing_runs_one_way() {
1001        // A network image asks the process-wide fetcher, which the network tests swap
1002        // and count: without their lock this ask lands in one of their counts.
1003        let _guard = network::serialised();
1004        frus_core::set_image_fetcher(|_url, _deliver| {});
1005        let ready = Image::new(handle(8, 8)).placeholder(Color::WHITE);
1006        assert_eq!(Widget::<()>::anim_target(&ready), Some(1.0));
1007        assert_eq!(Widget::<()>::anim_duration(&ready), IMAGE_FADE);
1008
1009        let waiting = Image::network("https://example.invalid/x.png").placeholder(Color::WHITE);
1010        assert_eq!(Widget::<()>::anim_target(&waiting), Some(0.0));
1011        assert_eq!(
1012            Widget::<()>::anim_duration(&waiting),
1013            0.0,
1014            "back to the placeholder in one frame"
1015        );
1016    }
1017
1018    /// **A rebuild does not restart it.** A view is rebuilt every frame here, so a
1019    /// crossing that began again each time would never finish — the picture would sit at
1020    /// its first step for ever.
1021    ///
1022    /// Nothing in the widget arranges that: the value lives against the widget's place in
1023    /// the tree and the target does not move while the source stands still, so the runtime
1024    /// carries it. Worth a test because it is the property the whole design turns on.
1025    #[test]
1026    fn a_rebuild_does_not_restart_the_crossing() {
1027        let image = || {
1028            Image::new(handle(8, 8))
1029                .size(50.0, 50.0)
1030                .placeholder(Color::WHITE)
1031        };
1032        let mut runtime = Runtime::default();
1033        // Mount: adopts, since there was no placeholder shown before the widget existed.
1034        runtime.advance_values::<()>(&image(), 0.0);
1035        let mounted = runtime.value(crate::interaction::WidgetId::ROOT);
1036        assert_eq!(mounted, 1.0, "an image already here does not fade in");
1037
1038        // Ten rebuilds of the same view, a frame apart.
1039        for _ in 0..10 {
1040            runtime.advance_values::<()>(&image(), 1.0 / 60.0);
1041        }
1042        assert_eq!(
1043            runtime.value(crate::interaction::WidgetId::ROOT),
1044            1.0,
1045            "and it has not gone back to the beginning"
1046        );
1047    }
1048
1049    /// **A picture takes the ambient icon size**, the same chain a path icon answers, so
1050    /// the two are interchangeable in a row of actions.
1051    #[test]
1052    fn an_image_icon_takes_the_ambient_icon_size() {
1053        let icon = ImageIcon::new(Image::new(handle(64, 64)));
1054        assert_eq!(
1055            Widget::<()>::style(&icon).width,
1056            frus_layout::Dimension::Length(crate::icons::GRID)
1057        );
1058        let mut theme = Theme::default();
1059        theme.widgets.icon.size = Some(18.0);
1060        assert_eq!(
1061            Widget::<()>::style_themed(&icon, &theme).width,
1062            frus_layout::Dimension::Length(18.0),
1063            "the theme has a say in the size, not only the colour"
1064        );
1065        assert_eq!(
1066            Widget::<()>::style_themed(&icon.size(30.0), &theme).width,
1067            frus_layout::Dimension::Length(30.0),
1068            "and the caller outranks it"
1069        );
1070    }
1071
1072    /// **A picture is not tinted unless the caller asks.** That is the one place this
1073    /// parts from a path icon, and it is deliberate: an icon is a silhouette and takes the
1074    /// foreground colour, where a brand mark flattened to one grey the first time an
1075    /// application themes its icons is a brand mark nobody recognises.
1076    #[test]
1077    fn a_picture_is_not_tinted_unless_it_is_asked() {
1078        let mut theme = Theme::default();
1079        theme.widgets.icon.color = Some(Color::rgb(0.9, 0.1, 0.1));
1080        let paint = |icon: ImageIcon| {
1081            let mut scene = frus_core::Scene::new();
1082            let status = Status::default();
1083            Widget::<()>::paint(
1084                &icon,
1085                Rect::new(0.0, 0.0, 24.0, 24.0),
1086                status,
1087                &theme,
1088                &mut scene,
1089            );
1090            scene
1091                .primitives()
1092                .iter()
1093                .find_map(|p| match p {
1094                    Primitive::Image { tint, .. } => Some(*tint),
1095                    _ => None,
1096                })
1097                .expect("a picture")
1098        };
1099        let plain = paint(ImageIcon::new(Image::new(handle(8, 8))));
1100        assert_eq!(
1101            (plain.r, plain.g, plain.b),
1102            (1.0, 1.0, 1.0),
1103            "its own colours, whatever the icon theme says"
1104        );
1105        let tinted =
1106            paint(ImageIcon::new(Image::new(handle(8, 8))).color(Color::rgb(0.0, 1.0, 0.0)));
1107        assert_eq!((tinted.r, tinted.g, tinted.b), (0.0, 1.0, 0.0));
1108    }
1109
1110    /// The box an image asks for, down a column 400 wide.
1111    ///
1112    /// A `Column` and not a `Flex`: the reference's column centres its children, where
1113    /// flexbox stretches them, and a **stretched** cross axis is a width the image was
1114    /// handed — which beats a ratio, here as there. That is the parent's say, not a
1115    /// property of the image.
1116    fn asked_box(image: Image) -> Rect {
1117        let root = crate::Column::<()>::new().child(image);
1118        let runtime = crate::Runtime::default();
1119        let theme = Theme::default();
1120        let mut layout = frus_layout::Layout::new();
1121        let node = crate::ui::build_layout(
1122            &root,
1123            crate::interaction::WidgetId::ROOT,
1124            &runtime,
1125            &theme,
1126            &mut layout,
1127        );
1128        layout.compute_filled(node, 400.0, 400.0);
1129        // The column is first; the image is its only child.
1130        layout.absolute_rects(node)[1].0
1131    }
1132
1133    /// No size given: the image's own, which is the whole point — showing a bitmap
1134    /// should not require already knowing how many pixels across it is.
1135    #[test]
1136    fn an_image_with_no_size_is_its_own_size() {
1137        let r = asked_box(Image::new(handle(64, 32)));
1138        assert_eq!((r.width, r.height), (64.0, 32.0));
1139    }
1140
1141    /// One side given: the other follows the image's ratio, as in the reference — as
1142    /// long as the parent is not handing that side a size of its own.
1143    #[test]
1144    fn one_side_derives_the_other_from_the_ratio() {
1145        let by_width = asked_box(Image::new(handle(64, 32)).width(128.0));
1146        assert_eq!((by_width.width, by_width.height), (128.0, 64.0));
1147
1148        let by_height = asked_box(Image::new(handle(64, 32)).height(8.0));
1149        assert_eq!((by_height.width, by_height.height), (16.0, 8.0));
1150    }
1151
1152    /// Both given: that box, ratio or no ratio.
1153    #[test]
1154    fn both_sides_given_win() {
1155        let r = asked_box(Image::new(handle(64, 32)).size(10.0, 90.0));
1156        assert_eq!((r.width, r.height), (10.0, 90.0));
1157    }
1158
1159    /// The alignment decides where a letterboxed image sits, and which part of a
1160    /// cropped one survives. Both from one anchor, which is the reference's answer too.
1161    #[test]
1162    fn the_alignment_places_the_letterbox_and_moves_the_crop() {
1163        let bounds = Rect::new(0.0, 0.0, 100.0, 40.0);
1164        // A square in a wide box, contained: 40×40, and the anchor says where across.
1165        let left = paint(
1166            Image::new(handle(10, 10))
1167                .size(100.0, 40.0)
1168                .alignment(Alignment::CENTER_LEFT),
1169            bounds,
1170        );
1171        let right = paint(
1172            Image::new(handle(10, 10))
1173                .size(100.0, 40.0)
1174                .alignment(Alignment::CENTER_RIGHT),
1175            bounds,
1176        );
1177        let x_of = |p: &Primitive| match p {
1178            Primitive::Image { rect, .. } => rect.x,
1179            other => panic!("an image, not {other:?}"),
1180        };
1181        assert_eq!(x_of(&left), 0.0);
1182        assert_eq!(x_of(&right), 60.0, "hard against the right edge");
1183
1184        // A tall image cropped to a wide box keeps the part the anchor names, which
1185        // travels the other way: aligning to the top keeps the top of the image.
1186        let uv_y = |p: &Primitive| match p {
1187            Primitive::Image { uv, .. } => uv.y,
1188            other => panic!("an image, not {other:?}"),
1189        };
1190        let top = paint(
1191            Image::new(handle(10, 100))
1192                .size(100.0, 40.0)
1193                .fit(BoxFit::Cover)
1194                .alignment(Alignment::TOP_CENTER),
1195            bounds,
1196        );
1197        let middle = paint(
1198            Image::new(handle(10, 100))
1199                .size(100.0, 40.0)
1200                .fit(BoxFit::Cover),
1201            bounds,
1202        );
1203        assert_eq!(uv_y(&top), 0.0, "the top of the image is kept");
1204        assert!(uv_y(&middle) > 0.0, "centred, the top is cropped away");
1205    }
1206
1207    /// Opacity goes into the tint rather than into a layer: an image is one primitive.
1208    #[test]
1209    fn opacity_fades_the_tint() {
1210        let faded = paint(
1211            Image::new(handle(4, 4)).size(20.0, 20.0).opacity(0.5),
1212            Rect::new(0.0, 0.0, 20.0, 20.0),
1213        );
1214        match faded {
1215            Primitive::Image { tint, .. } => assert_eq!(tint.a, 0.5),
1216            other => panic!("an image, not {other:?}"),
1217        }
1218    }
1219
1220    fn paint(image: Image, bounds: Rect) -> Primitive {
1221        paint_in(image, bounds, &Theme::default())
1222    }
1223
1224    /// The same, under a theme of the caller's choosing — which is how the reading
1225    /// direction reaches `paint`.
1226    fn paint_in(image: Image, bounds: Rect, theme: &Theme) -> Primitive {
1227        let mut scene = Scene::new();
1228        Widget::<()>::paint(&image, bounds, Status::default(), theme, &mut scene);
1229        scene.primitives()[0].clone()
1230    }
1231
1232    #[test]
1233    fn contain_letterboxes_a_square_in_a_wide_box() {
1234        // A square 10×10 source in a 100×40 box gives 40×40, centred in x.
1235        let prim = paint(
1236            Image::new(handle(10, 10)).size(100.0, 40.0),
1237            Rect::new(0.0, 0.0, 100.0, 40.0),
1238        );
1239        match prim {
1240            Primitive::Image { rect, uv, .. } => {
1241                assert_eq!(rect, Rect::new(30.0, 0.0, 40.0, 40.0));
1242                assert_eq!(uv, Rect::new(0.0, 0.0, 1.0, 1.0));
1243            }
1244            _ => panic!("expected an image"),
1245        }
1246    }
1247
1248    #[test]
1249    fn tint_override_is_applied() {
1250        let prim = paint(
1251            Image::new(handle(4, 4))
1252                .size(20.0, 20.0)
1253                .tint(Color::rgb(1.0, 0.0, 0.0)),
1254            Rect::new(0.0, 0.0, 20.0, 20.0),
1255        );
1256        match prim {
1257            Primitive::Image { tint, .. } => {
1258                assert_eq!(tint.r, 1.0);
1259                assert_eq!(tint.g, 0.0);
1260            }
1261            _ => panic!("expected an image"),
1262        }
1263    }
1264
1265    #[test]
1266    fn size_drives_the_layout_box() {
1267        let image = Image::new(handle(4, 4)).size(64.0, 48.0);
1268        let style = Widget::<()>::style(&image);
1269        assert_eq!(style.width, Dimension::Length(64.0));
1270        assert_eq!(style.height, Dimension::Length(48.0));
1271    }
1272
1273    /// The hole this milestone came for. `Role::Image` was in the vocabulary and mapped
1274    /// to the platform's, and **nothing in the framework ever emitted it** — so every
1275    /// picture in every application was silent to a screen reader.
1276    #[test]
1277    fn a_labelled_image_is_announced() {
1278        let semantics = Widget::<()>::semantics(&Image::new(handle(4, 4)).semantic_label("Ada"))
1279            .expect("an image is announced");
1280        assert_eq!(semantics.role, frus_core::Role::Image);
1281        assert_eq!(semantics.label.as_deref(), Some("Ada"));
1282    }
1283
1284    /// Unlabelled, it still says *there is a picture here*. A reader who meets it knows
1285    /// something is there and can move past it; leaving it out entirely would be the
1286    /// application's decision, not the widget's default.
1287    #[test]
1288    fn an_unlabelled_image_still_announces_the_role() {
1289        let semantics =
1290            Widget::<()>::semantics(&Image::new(handle(4, 4))).expect("still announced");
1291        assert_eq!(semantics.role, frus_core::Role::Image);
1292        assert_eq!(semantics.label, None);
1293    }
1294
1295    /// Decoration is excluded outright, and the exclusion wins over a label — the
1296    /// reference's rule, and the only one that is not ambiguous when both are given.
1297    #[test]
1298    fn decoration_is_left_out_of_the_tree_a_reader_walks() {
1299        let decoration = Image::new(handle(4, 4))
1300            .semantic_label("a texture")
1301            .exclude_from_semantics(true);
1302        assert!(Widget::<()>::semantics(&decoration).is_none());
1303    }
1304
1305    /// The sub-region an image samples, as painted.
1306    fn sampled(image: Image, theme: &Theme) -> Rect {
1307        match paint_in(image, Rect::new(0.0, 0.0, 40.0, 40.0), theme) {
1308            Primitive::Image { uv, .. } => uv,
1309            _ => panic!("expected an image"),
1310        }
1311    }
1312
1313    /// **The pixels, once they are here**, for painting them elsewhere; none while there
1314    /// are none (milestone 647).
1315    #[test]
1316    fn the_handle_is_the_pixels_once_here() {
1317        let pixels = handle(4, 2);
1318        assert_eq!(Image::new(pixels.clone()).handle(), Some(pixels));
1319        assert_eq!(Image::from_source(Source::Loading).handle(), None);
1320        assert_eq!(
1321            Image::from_source(Source::Failed("gone".into())).handle(),
1322            None
1323        );
1324    }
1325
1326    /// A picture that **points** is mirrored in a right-to-left reading direction, which
1327    /// the reference describes as a scaling factor of -1 horizontally. Here it is a sign
1328    /// on the sampled width: the shader reads `uv.xy + unit_pos * uv.zw`, so a negative
1329    /// width walks the same span backwards — no transform, no layer, no second copy of
1330    /// the pixels.
1331    #[test]
1332    fn a_directional_image_is_mirrored_in_rtl() {
1333        let forward = sampled(Image::new(handle(8, 4)), &Theme::default());
1334        let mirrored = sampled(
1335            Image::new(handle(8, 4)).match_text_direction(true),
1336            &Theme::default().rtl(),
1337        );
1338        assert_eq!(
1339            mirrored.width, -forward.width,
1340            "the span is walked backwards"
1341        );
1342        assert_eq!(
1343            mirrored.x,
1344            forward.x + forward.width,
1345            "and it starts at the far edge"
1346        );
1347        // The vertical span is untouched: this is a mirror, not a rotation.
1348        assert_eq!((mirrored.y, mirrored.height), (forward.y, forward.height));
1349    }
1350
1351    /// It is **opt-in**, and off it stays off in both directions. A photograph of a
1352    /// person does not want to be flipped because the interface is in Arabic, which is
1353    /// why the reference makes this a per-image switch rather than a global rule.
1354    #[test]
1355    fn an_ordinary_image_is_not_mirrored_by_the_reading_direction() {
1356        let plain = Image::new(handle(8, 4));
1357        assert_eq!(
1358            sampled(plain, &Theme::default().rtl()),
1359            sampled(Image::new(handle(8, 4)), &Theme::default()),
1360        );
1361    }
1362
1363    /// And the switch alone does nothing: it is the direction that mirrors, not the flag.
1364    #[test]
1365    fn a_directional_image_is_left_alone_in_ltr() {
1366        assert_eq!(
1367            sampled(
1368                Image::new(handle(8, 4)).match_text_direction(true),
1369                &Theme::default()
1370            ),
1371            sampled(Image::new(handle(8, 4)), &Theme::default()),
1372        );
1373    }
1374
1375    /// And it reaches the tree, not just the hook.
1376    ///
1377    /// A trait method nobody calls is the shape of bug this project has already been
1378    /// bitten by — a hook that answers correctly while the walk never asks it, green
1379    /// unit tests over a feature that does nothing. So this drives `build_ui` and reads
1380    /// what the walk actually collected.
1381    #[test]
1382    fn the_walk_collects_the_image_and_skips_the_decoration() {
1383        let tree = crate::Flex::<()>::column()
1384            .child(Image::new(handle(8, 8)).semantic_label("Ada Lovelace"))
1385            .child(Image::new(handle(8, 8)).exclude_from_semantics(true));
1386        let ui = crate::build_ui(
1387            &tree,
1388            crate::Size::new(200.0, 200.0),
1389            &crate::Runtime::default(),
1390            &Theme::default(),
1391        );
1392        let images: Vec<_> = ui
1393            .semantics()
1394            .iter()
1395            .filter(|(_, _, s)| s.role == frus_core::Role::Image)
1396            .map(|(_, _, s)| s.label.clone())
1397            .collect();
1398        assert_eq!(
1399            images,
1400            vec![Some("Ada Lovelace".to_string())],
1401            "the labelled one is announced and the decoration is not"
1402        );
1403    }
1404
1405    /// A 2x2 PNG, encoded at run time so the test carries no binary fixture.
1406    ///
1407    /// Leaked deliberately: [`frus_core::cached`] keys on the address of `'static`
1408    /// bytes, and this is how a test gets some. One 90-odd byte leak per test process is
1409    /// the price of exercising the real path rather than a stand-in.
1410    #[cfg(feature = "images")]
1411    fn png() -> &'static [u8] {
1412        let mut out = Vec::new();
1413        {
1414            let mut encoder = png::Encoder::new(&mut out, 2, 2);
1415            encoder.set_color(png::ColorType::Rgba);
1416            encoder.set_depth(png::BitDepth::Eight);
1417            let mut writer = encoder.write_header().expect("header");
1418            writer.write_image_data(&[255u8; 16]).expect("pixels");
1419        }
1420        Vec::leak(out)
1421    }
1422
1423    /// The question an application asks first: how do I show my logo. One call, and the
1424    /// pixels come out the far side at the size the file says.
1425    #[cfg(feature = "images")]
1426    #[test]
1427    fn embedded_bytes_become_pixels() {
1428        let image = Image::memory(png());
1429        assert_eq!(image.error(), None, "a valid PNG decodes");
1430        let style = Widget::<()>::style(&image);
1431        assert_eq!(
1432            style.width,
1433            Dimension::Length(2.0),
1434            "at the file's own size"
1435        );
1436    }
1437
1438    /// **Once** per process, not once per frame.
1439    ///
1440    /// This is the whole reason the store exists: a view is rebuilt sixty times a second
1441    /// and decoding a PNG is not free. The two handles being the same `Arc` is the proof
1442    /// — a second decode would produce a second `ImageData` with a different identity.
1443    #[cfg(feature = "images")]
1444    #[test]
1445    fn the_same_bytes_are_decoded_once() {
1446        let bytes = png();
1447        let first = Image::memory(bytes);
1448        let second = Image::memory(bytes);
1449        let handle = |image: &Image| match image.source() {
1450            Source::Ready(handle) => handle.clone(),
1451            _ => panic!("decoded"),
1452        };
1453        let (a, b) = (handle(&first), handle(&second));
1454        let (a, b) = (&a, &b);
1455        assert_eq!(a.id(), b.id(), "one decode, one image, shared");
1456        assert!(
1457            std::sync::Arc::ptr_eq(a, b),
1458            "and literally the same handle"
1459        );
1460    }
1461
1462    /// Bytes that are not an image: the widget says so and paints nothing, rather than
1463    /// panicking or drawing a stand-in nobody asked for.
1464    #[test]
1465    fn bytes_that_are_not_an_image_report_it_and_draw_nothing() {
1466        static NOT_AN_IMAGE: &[u8] = b"this is not a PNG";
1467        let broken = Image::memory(NOT_AN_IMAGE);
1468        assert!(broken.error().is_some(), "it says what went wrong");
1469        let mut scene = Scene::new();
1470        Widget::<()>::paint(
1471            &broken,
1472            Rect::new(0.0, 0.0, 40.0, 40.0),
1473            Status::default(),
1474            &Theme::default(),
1475            &mut scene,
1476        );
1477        assert!(scene.primitives().is_empty(), "and draws nothing at all");
1478    }
1479
1480    /// A broken image takes **no room** unless it was given some. There is no natural
1481    /// size to fall back on, and anything the caller did say is still honoured — which
1482    /// is what keeps a page from jumping because one asset is bad.
1483    #[test]
1484    fn a_broken_image_takes_the_room_it_was_given_and_no_more() {
1485        static NOT_AN_IMAGE: &[u8] = b"nor is this";
1486        let bare = Widget::<()>::style(&Image::memory(NOT_AN_IMAGE));
1487        assert_eq!(bare.width, Dimension::Length(0.0));
1488        assert_eq!(bare.height, Dimension::Length(0.0));
1489        let sized = Widget::<()>::style(&Image::memory(NOT_AN_IMAGE).size(64.0, 48.0));
1490        assert_eq!(sized.width, Dimension::Length(64.0));
1491        assert_eq!(sized.height, Dimension::Length(48.0));
1492    }
1493
1494    /// The failure is remembered too. A file that is not a PNG will not become one on
1495    /// the next frame, and retrying every frame would turn one broken asset into a
1496    /// permanent cost.
1497    #[test]
1498    fn a_failure_is_remembered_rather_than_retried() {
1499        static NOT_AN_IMAGE: &[u8] = b"still not a PNG";
1500        let first = Image::memory(NOT_AN_IMAGE).error().map(str::to_string);
1501        let second = Image::memory(NOT_AN_IMAGE).error().map(str::to_string);
1502        assert!(first.is_some());
1503        assert_eq!(first, second);
1504    }
1505
1506    /// The network path, with the network standing in for itself.
1507    ///
1508    /// These share one process-wide store and one registered fetcher, so they run under
1509    /// a lock rather than in parallel: two of them racing would see each other's URLs
1510    /// and each other's fetcher. That is a property of the thing being tested — it is
1511    /// deliberately global — rather than a weakness of the tests.
1512    #[cfg(feature = "images")]
1513    mod network {
1514        use super::*;
1515        use std::sync::MutexGuard;
1516
1517        pub(super) fn serialised() -> MutexGuard<'static, ()> {
1518            let guard = crate::remote::fetcher_lock();
1519            frus_core::forget_fetched_images();
1520            guard
1521        }
1522
1523        /// A fetcher that answers **on the spot**, with a PNG.
1524        ///
1525        /// Synchronous on purpose: it is the harder case for the store, since the
1526        /// callback runs while `fetched` is still inside the call that started it. If
1527        /// the lock were still held there, this would deadlock rather than fail.
1528        fn instant_png(_url: &str, deliver: Box<dyn FnOnce(Result<Vec<u8>, String>) + Send>) {
1529            deliver(Ok(png().to_vec()));
1530        }
1531
1532        /// A fetcher that answers on the spot with a failure.
1533        fn instant_failure(_url: &str, deliver: Box<dyn FnOnce(Result<Vec<u8>, String>) + Send>) {
1534            deliver(Err("404".to_string()));
1535        }
1536
1537        /// One that never answers at all — a request still in flight.
1538        fn never(_url: &str, _deliver: Box<dyn FnOnce(Result<Vec<u8>, String>) + Send>) {}
1539
1540        #[test]
1541        fn a_fetched_image_arrives_and_is_shown() {
1542            let _guard = serialised();
1543            frus_core::set_image_fetcher(instant_png);
1544            // The first ask started the work; this fetcher answers at once, and the bytes
1545            // are decoded on a decoding thread, so a later ask has the pixels.
1546            let _held = decoded(|| Image::network("https://example.com/a.png"));
1547            let arrived = Image::network("https://example.com/a.png");
1548            assert_eq!(arrived.state(), State::Ready);
1549            assert_eq!(Widget::<()>::style(&arrived).width, Dimension::Length(2.0));
1550        }
1551
1552        #[test]
1553        fn a_request_still_in_flight_reads_as_loading_and_keeps_the_frames_coming() {
1554            let _guard = serialised();
1555            frus_core::set_image_fetcher(never);
1556            let waiting = Image::network("https://example.com/slow.png");
1557            assert_eq!(waiting.state(), State::Loading);
1558            assert_eq!(waiting.error(), None, "loading has not failed");
1559            // The interface must keep drawing, or the frame that would show the picture
1560            // never happens.
1561            assert!(frus_core::images_in_flight() > 0);
1562        }
1563
1564        #[test]
1565        fn a_request_that_fails_says_why_rather_than_waiting_for_ever() {
1566            let _guard = serialised();
1567            frus_core::set_image_fetcher(instant_failure);
1568            let _ = Image::network("https://example.com/gone.png").state();
1569            let dead = Image::network("https://example.com/gone.png");
1570            assert_eq!(dead.state(), State::Failed("404"));
1571            assert_eq!(dead.error(), Some("404"));
1572        }
1573
1574        /// **Once**, however many frames ask. A view runs sixty times a second, and a
1575        /// fetch per frame would be sixty requests for one picture.
1576        #[test]
1577        fn a_view_asking_every_frame_fetches_once() {
1578            let _guard = serialised();
1579            static CALLS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
1580            fn counted(_url: &str, deliver: Box<dyn FnOnce(Result<Vec<u8>, String>) + Send>) {
1581                CALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1582                deliver(Ok(png().to_vec()));
1583            }
1584            CALLS.store(0, std::sync::atomic::Ordering::Relaxed);
1585            frus_core::set_image_fetcher(counted);
1586            for _ in 0..60 {
1587                let _ = Image::network("https://example.com/once.png").state();
1588            }
1589            assert_eq!(CALLS.load(std::sync::atomic::Ordering::Relaxed), 1);
1590        }
1591
1592        /// And nothing is left in flight once it has landed, so the interface settles
1593        /// back to drawing only when something changes.
1594        #[test]
1595        fn the_count_falls_back_to_zero_when_the_work_is_done() {
1596            let _guard = serialised();
1597            frus_core::set_image_fetcher(instant_png);
1598            let _ = decoded(|| Image::network("https://example.com/settle.png"));
1599            // The count is the whole process's, and the tests beside this one decode
1600            // pictures of their own: it falls to nought once theirs have landed too.
1601            let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
1602            while frus_core::images_in_flight() > 0 {
1603                assert!(
1604                    std::time::Instant::now() < deadline,
1605                    "something stays in flight"
1606                );
1607                std::thread::sleep(std::time::Duration::from_millis(2));
1608            }
1609        }
1610
1611        /// A large picture fetched into a small box arrives at the box's size.
1612        #[test]
1613        fn a_fetched_image_is_decoded_at_the_size_it_is_shown() {
1614            let _guard = serialised();
1615            fn big(_url: &str, deliver: Box<dyn FnOnce(Result<Vec<u8>, String>) + Send>) {
1616                deliver(Ok(big_png(1000, 500).to_vec()));
1617            }
1618            frus_core::set_image_fetcher(big);
1619            let handle = decoded(|| Image::network("https://example.com/big.png").size(40.0, 40.0));
1620            // 40 logical pixels at a ratio of 1, in steps of 32: 64, covered at 2:1.
1621            assert_eq!((handle.width(), handle.height()), (128, 64));
1622            // The same address at its own size is another entry, and another fetch.
1623            let full = decoded(|| Image::network("https://example.com/big.png"));
1624            assert_eq!(full.width(), 1000);
1625        }
1626    }
1627
1628    /// The pixels of the image `make` builds, once they are there: a large picture is
1629    /// decoded on a thread, so this asks again, as the frames that follow would, until it
1630    /// has them.
1631    #[cfg(feature = "images")]
1632    fn decoded(make: impl Fn() -> Image) -> frus_core::ImageHandle {
1633        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
1634        loop {
1635            let image = make();
1636            match image.state() {
1637                State::Ready => return image.handle().expect("ready"),
1638                State::Failed(why) => panic!("the image failed: {why}"),
1639                State::Loading => {
1640                    assert!(std::time::Instant::now() < deadline, "never decoded");
1641                    std::thread::sleep(std::time::Duration::from_millis(2));
1642                }
1643            }
1644        }
1645    }
1646
1647    /// A `width` × `height` PNG, leaked like [`png`].
1648    #[cfg(feature = "images")]
1649    fn big_png(width: u32, height: u32) -> &'static [u8] {
1650        let mut out = Vec::new();
1651        {
1652            let mut encoder = png::Encoder::new(&mut out, width, height);
1653            encoder.set_color(png::ColorType::Rgba);
1654            encoder.set_depth(png::BitDepth::Eight);
1655            let mut writer = encoder.write_header().expect("header");
1656            writer
1657                .write_image_data(&vec![200u8; (width * height * 4) as usize])
1658                .expect("pixels");
1659        }
1660        Vec::leak(out)
1661    }
1662
1663    /// **An image given a size is decoded at that size**, in physical pixels, and the
1664    /// picture keeps its own shape on the way.
1665    #[cfg(feature = "images")]
1666    #[test]
1667    fn an_embedded_image_given_a_size_is_decoded_at_it() {
1668        let bytes = big_png(600, 300);
1669        let at = |make: &dyn Fn() -> Image| {
1670            let handle = decoded(make);
1671            (handle.width(), handle.height())
1672        };
1673        // At a ratio of 1 the box is 96 logical pixels: 96, covered at 2:1.
1674        assert_eq!(at(&|| Image::memory(bytes).size(96.0, 96.0)), (192, 96));
1675        // At a ratio of 2, twice that.
1676        let dense =
1677            crate::MediaQuery::new(frus_core::Size::new(400.0, 800.0)).with_device_pixel_ratio(2.0);
1678        assert_eq!(
1679            dense.scope(|| at(&|| Image::memory(bytes).size(96.0, 96.0))),
1680            (384, 192)
1681        );
1682        // One side: the other from the ratio, and the layout keeps the ratio too.
1683        assert_eq!(at(&|| Image::memory(bytes).width(100.0)), (128, 64));
1684        // An explicit cache size wins over the box.
1685        assert_eq!(
1686            at(&|| Image::memory(bytes).size(96.0, 96.0).cache_width(60)),
1687            (60, 30)
1688        );
1689        // No size at all, a natural-size fit, or the opt-out: the file's own size.
1690        assert_eq!(at(&|| Image::memory(bytes)), (600, 300));
1691        assert_eq!(
1692            at(&|| Image::memory(bytes).size(96.0, 96.0).fit(BoxFit::None)),
1693            (600, 300)
1694        );
1695        assert_eq!(
1696            at(&|| Image::memory(bytes).size(96.0, 96.0).full_resolution()),
1697            (600, 300)
1698        );
1699        // Nothing larger than the file.
1700        assert_eq!(
1701            at(&|| Image::memory(bytes).size(4000.0, 4000.0)),
1702            (600, 300)
1703        );
1704    }
1705
1706    /// Asked before it was sized, an image is resolved again at the size it was given,
1707    /// and one built from pixels keeps its own.
1708    #[cfg(feature = "images")]
1709    #[test]
1710    fn sizing_after_asking_decodes_at_the_new_size() {
1711        let bytes = big_png(640, 320);
1712        // Held, so a trim in a test beside this one cannot take them back in between.
1713        let full = decoded(|| Image::memory(bytes));
1714        assert_eq!(full.width(), 640);
1715        let _small = decoded(|| Image::memory(bytes).width(64.0));
1716        let image = Image::memory(bytes);
1717        assert!(image.handle().is_some(), "asked at the file's size");
1718        let image = image.width(64.0);
1719        assert_eq!(image.handle().map(|h| h.width()), Some(64));
1720        let given = frus_core::ImageData::from_rgba(4, 2, vec![0; 32]).into_handle();
1721        let image = Image::new(given.clone()).width(2.0);
1722        assert!(std::sync::Arc::ptr_eq(&image.handle().unwrap(), &given));
1723    }
1724
1725    /// The decoded size is the shown size, not a new one: an image given only a width
1726    /// lays out at the ratio of the file, and an unsized one with a cache size takes the
1727    /// size of what was decoded, as the documentation says.
1728    #[cfg(feature = "images")]
1729    #[test]
1730    fn a_smaller_decode_keeps_the_layout() {
1731        let bytes = big_png(600, 300);
1732        // Held, so a trim in a test beside this one cannot take them back in between.
1733        let _wide = decoded(|| Image::memory(bytes).width(100.0));
1734        let _cached = decoded(|| Image::memory(bytes).cache_width(50));
1735        let style = Widget::<()>::style(&Image::memory(bytes).width(100.0));
1736        assert_eq!(style.width, Dimension::Length(100.0));
1737        assert_eq!(style.aspect_ratio, Some(2.0));
1738        let style = Widget::<()>::style(&Image::memory(bytes).cache_width(50));
1739        assert_eq!(style.width, Dimension::Length(50.0));
1740    }
1741
1742    /// The two sizes of one asset are two entries, each decoded once.
1743    #[cfg(feature = "images")]
1744    #[test]
1745    fn each_size_is_decoded_once() {
1746        let bytes = big_png(300, 300);
1747        let a = decoded(|| Image::memory(bytes).size(50.0, 50.0));
1748        let b = decoded(|| Image::memory(bytes).size(50.0, 50.0));
1749        let c = decoded(|| Image::memory(bytes).size(90.0, 90.0));
1750        assert!(
1751            std::sync::Arc::ptr_eq(&a, &b),
1752            "the same size is the same decode"
1753        );
1754        assert_ne!(a.id(), c.id(), "another size is another");
1755        assert_eq!((c.width(), c.height()), (96, 96));
1756    }
1757
1758    /// **A large embedded picture does not hold the frame up**: it is loading while a
1759    /// thread decodes it, shows its placeholder meanwhile, keeps the frames coming, and
1760    /// arrives; a small one is there at once.
1761    #[cfg(feature = "images")]
1762    #[test]
1763    fn a_large_embedded_picture_is_decoded_on_a_thread() {
1764        let bytes = big_png(700, 700);
1765        let first = Image::memory(bytes).size(50.0, 50.0);
1766        // A decode this size takes longer than the call that started it.
1767        if first.state() == State::Loading {
1768            assert!(frus_core::images_in_flight() > 0, "the frames keep coming");
1769        }
1770        let arrived = decoded(|| Image::memory(bytes).size(50.0, 50.0));
1771        assert_eq!(arrived.width(), 64);
1772        let small = Image::memory(big_png(200, 200));
1773        assert_eq!(
1774            small.state(),
1775            State::Ready,
1776            "an icon-sized picture is there at once"
1777        );
1778    }
1779
1780    /// Low memory gives back what nobody shows, and keeps what is on screen.
1781    #[cfg(feature = "images")]
1782    #[test]
1783    fn trimming_keeps_what_is_shown_and_drops_the_rest() {
1784        let bytes = big_png(200, 200);
1785        let shown = Image::memory(bytes).handle().unwrap();
1786        let unshown = Image::memory(bytes).size(30.0, 30.0).handle().unwrap().id();
1787        frus_core::trim_image_caches();
1788        assert!(
1789            std::sync::Arc::ptr_eq(&shown, &Image::memory(bytes).handle().unwrap()),
1790            "the one held is kept"
1791        );
1792        assert_ne!(
1793            Image::memory(bytes).size(30.0, 30.0).handle().unwrap().id(),
1794            unshown,
1795            "the one nobody held is decoded again"
1796        );
1797    }
1798}