egui/widgets/
image.rs

1use std::{borrow::Cow, slice::Iter, sync::Arc, time::Duration};
2
3use emath::{Align, Float as _, GuiRounding as _, NumExt as _, Rot2};
4use epaint::{
5    RectShape,
6    text::{LayoutJob, TextFormat, TextWrapping},
7};
8
9use crate::{
10    Color32, Context, CornerRadius, Id, Mesh, Painter, Rect, Response, Sense, Shape, Spinner,
11    TextStyle, TextureOptions, Ui, Vec2, Widget, WidgetInfo, WidgetType,
12    load::{Bytes, SizeHint, SizedTexture, TextureLoadResult, TexturePoll},
13    pos2,
14};
15
16/// A widget which displays an image.
17///
18/// The task of actually loading the image is deferred to when the `Image` is added to the [`Ui`],
19/// and how it is loaded depends on the provided [`ImageSource`]:
20///
21/// - [`ImageSource::Uri`] will load the image using the [asynchronous loading process][`crate::load`].
22/// - [`ImageSource::Bytes`] will also load the image using the [asynchronous loading process][`crate::load`], but with lower latency.
23/// - [`ImageSource::Texture`] will use the provided texture.
24///
25/// See [`crate::load`] for more information.
26///
27/// ### Examples
28/// // Using it in a layout:
29/// ```
30/// # egui::__run_test_ui(|ui| {
31/// ui.add(
32///     egui::Image::new(egui::include_image!("../../assets/ferris.png"))
33///         .corner_radius(5)
34/// );
35/// # });
36/// ```
37///
38/// // Using it just to paint:
39/// ```
40/// # egui::__run_test_ui(|ui| {
41/// # let rect = egui::Rect::from_min_size(Default::default(), egui::Vec2::splat(100.0));
42/// egui::Image::new(egui::include_image!("../../assets/ferris.png"))
43///     .corner_radius(5)
44///     .tint(egui::Color32::LIGHT_BLUE)
45///     .paint_at(ui, rect);
46/// # });
47/// ```
48///
49#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
50#[derive(Debug, Clone)]
51pub struct Image<'a> {
52    source: ImageSource<'a>,
53    texture_options: TextureOptions,
54    image_options: ImageOptions,
55    sense: Sense,
56    size: ImageSize,
57    pub(crate) show_loading_spinner: Option<bool>,
58    pub(crate) alt_text: Option<String>,
59}
60
61impl<'a> Image<'a> {
62    /// Load the image from some source.
63    pub fn new(source: impl Into<ImageSource<'a>>) -> Self {
64        fn new_mono(source: ImageSource<'_>) -> Image<'_> {
65            let size = if let ImageSource::Texture(tex) = &source {
66                // User is probably expecting their texture to have
67                // the exact size of the provided `SizedTexture`.
68                ImageSize {
69                    maintain_aspect_ratio: true,
70                    max_size: Vec2::INFINITY,
71                    fit: ImageFit::Exact(tex.size),
72                }
73            } else {
74                Default::default()
75            };
76
77            Image {
78                source,
79                texture_options: Default::default(),
80                image_options: Default::default(),
81                sense: Sense::hover(),
82                size,
83                show_loading_spinner: None,
84                alt_text: None,
85            }
86        }
87
88        new_mono(source.into())
89    }
90
91    /// Load the image from a URI.
92    ///
93    /// See [`ImageSource::Uri`].
94    pub fn from_uri(uri: impl Into<Cow<'a, str>>) -> Self {
95        Self::new(ImageSource::Uri(uri.into()))
96    }
97
98    /// Load the image from an existing texture.
99    ///
100    /// See [`ImageSource::Texture`].
101    pub fn from_texture(texture: impl Into<SizedTexture>) -> Self {
102        Self::new(ImageSource::Texture(texture.into()))
103    }
104
105    /// Load the image from some raw bytes.
106    ///
107    /// For better error messages, use the `bytes://` prefix for the URI.
108    ///
109    /// See [`ImageSource::Bytes`].
110    pub fn from_bytes(uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) -> Self {
111        Self::new(ImageSource::Bytes {
112            uri: uri.into(),
113            bytes: bytes.into(),
114        })
115    }
116
117    /// Texture options used when creating the texture.
118    #[inline]
119    pub fn texture_options(mut self, texture_options: TextureOptions) -> Self {
120        self.texture_options = texture_options;
121        self
122    }
123
124    /// Set the max width of the image.
125    ///
126    /// No matter what the image is scaled to, it will never exceed this limit.
127    #[inline]
128    pub fn max_width(mut self, width: f32) -> Self {
129        self.size.max_size.x = width;
130        self
131    }
132
133    /// Set the max height of the image.
134    ///
135    /// No matter what the image is scaled to, it will never exceed this limit.
136    #[inline]
137    pub fn max_height(mut self, height: f32) -> Self {
138        self.size.max_size.y = height;
139        self
140    }
141
142    /// Set the max size of the image.
143    ///
144    /// No matter what the image is scaled to, it will never exceed this limit.
145    #[inline]
146    pub fn max_size(mut self, size: Vec2) -> Self {
147        self.size.max_size = size;
148        self
149    }
150
151    /// Whether or not the [`ImageFit`] should maintain the image's original aspect ratio.
152    #[inline]
153    pub fn maintain_aspect_ratio(mut self, value: bool) -> Self {
154        self.size.maintain_aspect_ratio = value;
155        self
156    }
157
158    /// Fit the image to its original size with some scaling.
159    ///
160    /// The texel size of the source image will be multiplied by the `scale` factor,
161    /// and then become the _ui_ size of the [`Image`].
162    ///
163    /// This will cause the image to overflow if it is larger than the available space.
164    ///
165    /// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit.
166    #[inline]
167    pub fn fit_to_original_size(mut self, scale: f32) -> Self {
168        self.size.fit = ImageFit::Original { scale };
169        self
170    }
171
172    /// Fit the image to an exact size.
173    ///
174    /// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit.
175    #[inline]
176    pub fn fit_to_exact_size(mut self, size: Vec2) -> Self {
177        self.size.fit = ImageFit::Exact(size);
178        self
179    }
180
181    /// Fit the image to a fraction of the available space.
182    ///
183    /// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit.
184    #[inline]
185    pub fn fit_to_fraction(mut self, fraction: Vec2) -> Self {
186        self.size.fit = ImageFit::Fraction(fraction);
187        self
188    }
189
190    /// Fit the image to 100% of its available size, shrinking it if necessary.
191    ///
192    /// This is a shorthand for [`Image::fit_to_fraction`] with `1.0` for both width and height.
193    ///
194    /// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit.
195    #[inline]
196    pub fn shrink_to_fit(self) -> Self {
197        self.fit_to_fraction(Vec2::new(1.0, 1.0))
198    }
199
200    /// Make the image respond to clicks and/or drags.
201    #[inline]
202    pub fn sense(mut self, sense: Sense) -> Self {
203        self.sense = sense;
204        self
205    }
206
207    /// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
208    #[inline]
209    pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
210        self.image_options.uv = uv.into();
211        self
212    }
213
214    /// A solid color to put behind the image. Useful for transparent images.
215    #[inline]
216    pub fn bg_fill(mut self, bg_fill: impl Into<Color32>) -> Self {
217        self.image_options.bg_fill = bg_fill.into();
218        self
219    }
220
221    /// Multiply image color with this. Default is WHITE (no tint).
222    #[inline]
223    pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
224        self.image_options.tint = tint.into();
225        self
226    }
227
228    /// Rotate the image about an origin by some angle
229    ///
230    /// Positive angle is clockwise.
231    /// Origin is a vector in normalized UV space ((0,0) in top-left, (1,1) bottom right).
232    ///
233    /// To rotate about the center you can pass `Vec2::splat(0.5)` as the origin.
234    ///
235    /// Due to limitations in the current implementation,
236    /// this will turn off rounding of the image.
237    #[inline]
238    pub fn rotate(mut self, angle: f32, origin: Vec2) -> Self {
239        self.image_options.rotation = Some((Rot2::from_angle(angle), origin));
240        self.image_options.corner_radius = CornerRadius::ZERO; // incompatible with rotation
241        self
242    }
243
244    /// Round the corners of the image.
245    ///
246    /// The default is no rounding ([`CornerRadius::ZERO`]).
247    ///
248    /// Due to limitations in the current implementation,
249    /// this will turn off any rotation of the image.
250    #[inline]
251    pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
252        self.image_options.corner_radius = corner_radius.into();
253        if self.image_options.corner_radius != CornerRadius::ZERO {
254            self.image_options.rotation = None; // incompatible with rounding
255        }
256        self
257    }
258
259    /// Round the corners of the image.
260    ///
261    /// The default is no rounding ([`CornerRadius::ZERO`]).
262    ///
263    /// Due to limitations in the current implementation,
264    /// this will turn off any rotation of the image.
265    #[inline]
266    #[deprecated = "Renamed to `corner_radius`"]
267    pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
268        self.corner_radius(corner_radius)
269    }
270
271    /// Show a spinner when the image is loading.
272    ///
273    /// By default this uses the value of [`crate::Visuals::image_loading_spinners`].
274    #[inline]
275    pub fn show_loading_spinner(mut self, show: bool) -> Self {
276        self.show_loading_spinner = Some(show);
277        self
278    }
279
280    /// Set alt text for the image. This will be shown when the image fails to load.
281    ///
282    /// It will also be used for accessibility (e.g. read by screen readers).
283    #[inline]
284    pub fn alt_text(mut self, label: impl Into<String>) -> Self {
285        self.alt_text = Some(label.into());
286        self
287    }
288}
289
290impl<'a, T: Into<ImageSource<'a>>> From<T> for Image<'a> {
291    fn from(value: T) -> Self {
292        Image::new(value)
293    }
294}
295
296impl<'a> Image<'a> {
297    /// Returns the size the image will occupy in the final UI.
298    #[inline]
299    pub fn calc_size(&self, available_size: Vec2, image_source_size: Option<Vec2>) -> Vec2 {
300        let image_source_size = image_source_size.unwrap_or(Vec2::splat(24.0)); // Fallback for still-loading textures, or failure to load.
301        self.size.calc_size(available_size, image_source_size)
302    }
303
304    pub fn load_and_calc_size(&self, ui: &Ui, available_size: Vec2) -> Option<Vec2> {
305        let image_size = self.load_for_size(ui.ctx(), available_size).ok()?.size()?;
306        Some(self.size.calc_size(available_size, image_size))
307    }
308
309    #[inline]
310    pub fn size(&self) -> Option<Vec2> {
311        match &self.source {
312            ImageSource::Texture(texture) => Some(texture.size),
313            ImageSource::Uri(_) | ImageSource::Bytes { .. } => None,
314        }
315    }
316
317    /// Returns the URI of the image.
318    ///
319    /// For animated images, returns the URI without the frame number.
320    #[inline]
321    pub fn uri(&self) -> Option<&str> {
322        let uri = self.source.uri()?;
323
324        if let Ok((gif_uri, _index)) = decode_animated_image_uri(uri) {
325            Some(gif_uri)
326        } else {
327            Some(uri)
328        }
329    }
330
331    #[inline]
332    pub fn image_options(&self) -> &ImageOptions {
333        &self.image_options
334    }
335
336    #[inline]
337    pub fn source(&'a self, ctx: &Context) -> ImageSource<'a> {
338        match &self.source {
339            ImageSource::Uri(uri) if is_animated_image_uri(uri) => {
340                let frame_uri =
341                    encode_animated_image_uri(uri, animated_image_frame_index(ctx, uri));
342                ImageSource::Uri(Cow::Owned(frame_uri))
343            }
344
345            ImageSource::Bytes { uri, bytes } if are_animated_image_bytes(bytes) => {
346                let frame_uri =
347                    encode_animated_image_uri(uri, animated_image_frame_index(ctx, uri));
348                ctx.include_bytes(uri.clone(), bytes.clone());
349                ImageSource::Uri(Cow::Owned(frame_uri))
350            }
351            _ => self.source.clone(),
352        }
353    }
354
355    /// Load the image from its [`Image::source`], returning the resulting [`SizedTexture`].
356    ///
357    /// The `available_size` is used as a hint when e.g. rendering an svg.
358    ///
359    /// # Errors
360    /// May fail if they underlying [`Context::try_load_texture`] call fails.
361    pub fn load_for_size(&self, ctx: &Context, available_size: Vec2) -> TextureLoadResult {
362        let size_hint = self.size.hint(available_size, ctx.pixels_per_point());
363        self.source(ctx)
364            .clone()
365            .load(ctx, self.texture_options, size_hint)
366    }
367
368    /// Paint the image in the given rectangle.
369    ///
370    /// ```
371    /// # egui::__run_test_ui(|ui| {
372    /// # let rect = egui::Rect::from_min_size(Default::default(), egui::Vec2::splat(100.0));
373    /// egui::Image::new(egui::include_image!("../../assets/ferris.png"))
374    ///     .corner_radius(5)
375    ///     .tint(egui::Color32::LIGHT_BLUE)
376    ///     .paint_at(ui, rect);
377    /// # });
378    /// ```
379    #[inline]
380    pub fn paint_at(&self, ui: &Ui, rect: Rect) {
381        let pixels_per_point = ui.pixels_per_point();
382
383        let rect = rect.round_to_pixels(pixels_per_point);
384
385        // Load exactly the size of the rectangle we are painting to.
386        // This is important for getting crisp SVG:s.
387        let pixel_size = (pixels_per_point * rect.size()).round();
388
389        let texture = self.source(ui.ctx()).clone().load(
390            ui.ctx(),
391            self.texture_options,
392            SizeHint::Size {
393                width: pixel_size.x as _,
394                height: pixel_size.y as _,
395                maintain_aspect_ratio: false, // no - just get exactly what we asked for
396            },
397        );
398
399        paint_texture_load_result(
400            ui,
401            &texture,
402            rect,
403            self.show_loading_spinner,
404            &self.image_options,
405            self.alt_text.as_deref(),
406        );
407    }
408}
409
410impl Widget for Image<'_> {
411    fn ui(self, ui: &mut Ui) -> Response {
412        let tlr = self.load_for_size(ui.ctx(), ui.available_size());
413        let image_source_size = tlr.as_ref().ok().and_then(|t| t.size());
414        let ui_size = self.calc_size(ui.available_size(), image_source_size);
415
416        let (rect, response) = ui.allocate_exact_size(ui_size, self.sense);
417        response.widget_info(|| {
418            let mut info = WidgetInfo::new(WidgetType::Image);
419            info.label = self.alt_text.clone();
420            info
421        });
422        if ui.is_rect_visible(rect) {
423            paint_texture_load_result(
424                ui,
425                &tlr,
426                rect,
427                self.show_loading_spinner,
428                &self.image_options,
429                self.alt_text.as_deref(),
430            );
431        }
432        texture_load_result_response(&self.source(ui.ctx()), &tlr, response)
433    }
434}
435
436/// This type determines the constraints on how
437/// the size of an image should be calculated.
438#[derive(Debug, Clone, Copy)]
439pub struct ImageSize {
440    /// Whether or not the final size should maintain the original aspect ratio.
441    ///
442    /// This setting is applied last.
443    ///
444    /// This defaults to `true`.
445    pub maintain_aspect_ratio: bool,
446
447    /// Determines the maximum size of the image.
448    ///
449    /// Defaults to `Vec2::INFINITY` (no limit).
450    pub max_size: Vec2,
451
452    /// Determines how the image should shrink/expand/stretch/etc. to fit within its allocated space.
453    ///
454    /// This setting is applied first.
455    ///
456    /// Defaults to `ImageFit::Fraction([1, 1])`
457    pub fit: ImageFit,
458}
459
460/// This type determines how the image should try to fit within the UI.
461///
462/// The final fit will be clamped to [`ImageSize::max_size`].
463#[derive(Debug, Clone, Copy)]
464#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
465pub enum ImageFit {
466    /// Fit the image to its original srce size, scaled by some factor.
467    ///
468    /// The original size of the image is usually its texel resolution,
469    /// but for an SVG it's the point size of the SVG.
470    ///
471    /// Ignores how much space is actually available in the ui.
472    Original { scale: f32 },
473
474    /// Fit the image to a fraction of the available size.
475    Fraction(Vec2),
476
477    /// Fit the image to an exact size.
478    ///
479    /// Ignores how much space is actually available in the ui.
480    Exact(Vec2),
481}
482
483impl ImageFit {
484    pub fn resolve(self, available_size: Vec2, image_size: Vec2) -> Vec2 {
485        match self {
486            Self::Original { scale } => image_size * scale,
487            Self::Fraction(fract) => available_size * fract,
488            Self::Exact(size) => size,
489        }
490    }
491}
492
493impl ImageSize {
494    /// Size hint for e.g. rasterizing an svg.
495    pub fn hint(&self, available_size: Vec2, pixels_per_point: f32) -> SizeHint {
496        let Self {
497            maintain_aspect_ratio,
498            max_size,
499            fit,
500        } = *self;
501
502        let point_size = match fit {
503            ImageFit::Original { scale } => {
504                return SizeHint::Scale((pixels_per_point * scale).ord());
505            }
506            ImageFit::Fraction(fract) => available_size * fract,
507            ImageFit::Exact(size) => size,
508        };
509        let point_size = point_size.at_most(max_size);
510
511        let pixel_size = pixels_per_point * point_size;
512
513        // `inf` on an axis means "any value"
514        match (pixel_size.x.is_finite(), pixel_size.y.is_finite()) {
515            (true, true) => SizeHint::Size {
516                width: pixel_size.x.round() as u32,
517                height: pixel_size.y.round() as u32,
518                maintain_aspect_ratio,
519            },
520            (true, false) => SizeHint::Width(pixel_size.x.round() as u32),
521            (false, true) => SizeHint::Height(pixel_size.y.round() as u32),
522            (false, false) => SizeHint::Scale(pixels_per_point.ord()),
523        }
524    }
525
526    /// Calculate the final on-screen size in points.
527    pub fn calc_size(&self, available_size: Vec2, image_source_size: Vec2) -> Vec2 {
528        let Self {
529            maintain_aspect_ratio,
530            max_size,
531            fit,
532        } = *self;
533        match fit {
534            ImageFit::Original { scale } => {
535                let image_size = scale * image_source_size;
536                if image_size.x <= max_size.x && image_size.y <= max_size.y {
537                    image_size
538                } else {
539                    scale_to_fit(image_size, max_size, maintain_aspect_ratio)
540                }
541            }
542            ImageFit::Fraction(fract) => {
543                let scale_to_size = (available_size * fract).min(max_size);
544                scale_to_fit(image_source_size, scale_to_size, maintain_aspect_ratio)
545            }
546            ImageFit::Exact(size) => {
547                let scale_to_size = size.min(max_size);
548                scale_to_fit(image_source_size, scale_to_size, maintain_aspect_ratio)
549            }
550        }
551    }
552}
553
554// TODO(jprochazk): unit-tests
555fn scale_to_fit(image_size: Vec2, available_size: Vec2, maintain_aspect_ratio: bool) -> Vec2 {
556    if maintain_aspect_ratio {
557        let ratio_x = available_size.x / image_size.x;
558        let ratio_y = available_size.y / image_size.y;
559        let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y };
560        let ratio = if ratio.is_finite() { ratio } else { 1.0 };
561        image_size * ratio
562    } else {
563        available_size
564    }
565}
566
567impl Default for ImageSize {
568    #[inline]
569    fn default() -> Self {
570        Self {
571            max_size: Vec2::INFINITY,
572            fit: ImageFit::Fraction(Vec2::new(1.0, 1.0)),
573            maintain_aspect_ratio: true,
574        }
575    }
576}
577
578/// This type tells the [`Ui`] how to load an image.
579///
580/// This is used by [`Image::new`] and [`Ui::image`].
581#[derive(Clone)]
582pub enum ImageSource<'a> {
583    /// Load the image from a URI, e.g. `https://example.com/image.png`.
584    ///
585    /// This could be a `file://` path, `https://` url, `bytes://` identifier, or some other scheme.
586    ///
587    /// How the URI will be turned into a texture for rendering purposes is
588    /// up to the registered loaders to handle.
589    ///
590    /// See [`crate::load`] for more information.
591    Uri(Cow<'a, str>),
592
593    /// Load the image from an existing texture.
594    ///
595    /// The user is responsible for loading the texture, determining its size,
596    /// and allocating a [`crate::TextureId`] for it.
597    Texture(SizedTexture),
598
599    /// Load the image from some raw bytes.
600    ///
601    /// The [`Bytes`] may be:
602    /// - `'static`, obtained from `include_bytes!` or similar
603    /// - Anything that can be converted to `Arc<[u8]>`
604    ///
605    /// This instructs the [`Ui`] to cache the raw bytes, which are then further processed by any registered loaders.
606    ///
607    /// See also [`crate::include_image`] for an easy way to load and display static images.
608    ///
609    /// See [`crate::load`] for more information.
610    Bytes {
611        /// The unique identifier for this image, e.g. `bytes://my_logo.png`.
612        ///
613        /// You should use a proper extension (`.jpg`, `.png`, `.svg`, etc) for the image to load properly.
614        ///
615        /// Use the `bytes://` scheme for the URI for better error messages.
616        uri: Cow<'static, str>,
617
618        bytes: Bytes,
619    },
620}
621
622impl std::fmt::Debug for ImageSource<'_> {
623    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
624        match self {
625            ImageSource::Bytes { uri, .. } | ImageSource::Uri(uri) => uri.as_ref().fmt(f),
626            ImageSource::Texture(st) => st.id.fmt(f),
627        }
628    }
629}
630
631impl ImageSource<'_> {
632    /// Size of the texture, if known.
633    #[inline]
634    pub fn texture_size(&self) -> Option<Vec2> {
635        match self {
636            ImageSource::Texture(texture) => Some(texture.size),
637            ImageSource::Uri(_) | ImageSource::Bytes { .. } => None,
638        }
639    }
640
641    /// # Errors
642    /// Failure to load the texture.
643    pub fn load(
644        self,
645        ctx: &Context,
646        texture_options: TextureOptions,
647        size_hint: SizeHint,
648    ) -> TextureLoadResult {
649        match self {
650            Self::Texture(texture) => Ok(TexturePoll::Ready { texture }),
651            Self::Uri(uri) => ctx.try_load_texture(uri.as_ref(), texture_options, size_hint),
652            Self::Bytes { uri, bytes } => {
653                ctx.include_bytes(uri.clone(), bytes);
654                ctx.try_load_texture(uri.as_ref(), texture_options, size_hint)
655            }
656        }
657    }
658
659    /// Get the `uri` that this image was constructed from.
660    ///
661    /// This will return `None` for [`Self::Texture`].
662    pub fn uri(&self) -> Option<&str> {
663        match self {
664            ImageSource::Bytes { uri, .. } | ImageSource::Uri(uri) => Some(uri),
665            ImageSource::Texture(_) => None,
666        }
667    }
668}
669
670pub fn paint_texture_load_result(
671    ui: &Ui,
672    tlr: &TextureLoadResult,
673    rect: Rect,
674    show_loading_spinner: Option<bool>,
675    options: &ImageOptions,
676    alt_text: Option<&str>,
677) {
678    match tlr {
679        Ok(TexturePoll::Ready { texture }) => {
680            paint_texture_at(ui.painter(), rect, options, texture);
681        }
682        Ok(TexturePoll::Pending { .. }) => {
683            let show_loading_spinner =
684                show_loading_spinner.unwrap_or(ui.visuals().image_loading_spinners);
685            if show_loading_spinner {
686                Spinner::new().paint_at(ui, rect);
687            }
688        }
689        Err(_) => {
690            let font_id = TextStyle::Body.resolve(ui.style());
691            let mut job = LayoutJob {
692                wrap: TextWrapping::truncate_at_width(rect.width()),
693                halign: Align::Center,
694                ..Default::default()
695            };
696            job.append(
697                "⚠",
698                0.0,
699                TextFormat::simple(font_id.clone(), ui.visuals().error_fg_color),
700            );
701            if let Some(alt_text) = alt_text {
702                job.append(
703                    alt_text,
704                    ui.spacing().item_spacing.x,
705                    TextFormat::simple(font_id, ui.visuals().text_color()),
706                );
707            }
708            let galley = ui.painter().layout_job(job);
709            ui.painter().galley(
710                rect.center() - Vec2::Y * galley.size().y * 0.5,
711                galley,
712                ui.visuals().text_color(),
713            );
714        }
715    }
716}
717
718/// Attach tooltips like "Loading…" or "Failed loading: …".
719pub fn texture_load_result_response(
720    source: &ImageSource<'_>,
721    tlr: &TextureLoadResult,
722    response: Response,
723) -> Response {
724    match tlr {
725        Ok(TexturePoll::Ready { .. }) => response,
726        Ok(TexturePoll::Pending { .. }) => {
727            let uri = source.uri().unwrap_or("image");
728            response.on_hover_text(format!("Loading {uri}…"))
729        }
730        Err(err) => {
731            let uri = source.uri().unwrap_or("image");
732            response.on_hover_text(format!("Failed loading {uri}: {err}"))
733        }
734    }
735}
736
737impl<'a> From<&'a str> for ImageSource<'a> {
738    #[inline]
739    fn from(value: &'a str) -> Self {
740        Self::Uri(value.into())
741    }
742}
743
744impl<'a> From<&'a String> for ImageSource<'a> {
745    #[inline]
746    fn from(value: &'a String) -> Self {
747        Self::Uri(value.as_str().into())
748    }
749}
750
751impl From<String> for ImageSource<'static> {
752    fn from(value: String) -> Self {
753        Self::Uri(value.into())
754    }
755}
756
757impl<'a> From<&'a Cow<'a, str>> for ImageSource<'a> {
758    #[inline]
759    fn from(value: &'a Cow<'a, str>) -> Self {
760        Self::Uri(value.clone())
761    }
762}
763
764impl<'a> From<Cow<'a, str>> for ImageSource<'a> {
765    #[inline]
766    fn from(value: Cow<'a, str>) -> Self {
767        Self::Uri(value)
768    }
769}
770
771impl<T: Into<Bytes>> From<(&'static str, T)> for ImageSource<'static> {
772    #[inline]
773    fn from((uri, bytes): (&'static str, T)) -> Self {
774        Self::Bytes {
775            uri: uri.into(),
776            bytes: bytes.into(),
777        }
778    }
779}
780
781impl<T: Into<Bytes>> From<(Cow<'static, str>, T)> for ImageSource<'static> {
782    #[inline]
783    fn from((uri, bytes): (Cow<'static, str>, T)) -> Self {
784        Self::Bytes {
785            uri,
786            bytes: bytes.into(),
787        }
788    }
789}
790
791impl<T: Into<Bytes>> From<(String, T)> for ImageSource<'static> {
792    #[inline]
793    fn from((uri, bytes): (String, T)) -> Self {
794        Self::Bytes {
795            uri: uri.into(),
796            bytes: bytes.into(),
797        }
798    }
799}
800
801impl<T: Into<SizedTexture>> From<T> for ImageSource<'static> {
802    fn from(value: T) -> Self {
803        Self::Texture(value.into())
804    }
805}
806
807#[derive(Debug, Clone)]
808#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
809pub struct ImageOptions {
810    /// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
811    pub uv: Rect,
812
813    /// A solid color to put behind the image. Useful for transparent images.
814    pub bg_fill: Color32,
815
816    /// Multiply image color with this. Default is WHITE (no tint).
817    pub tint: Color32,
818
819    /// Rotate the image about an origin by some angle
820    ///
821    /// Positive angle is clockwise.
822    /// Origin is a vector in normalized UV space ((0,0) in top-left, (1,1) bottom right).
823    ///
824    /// To rotate about the center you can pass `Vec2::splat(0.5)` as the origin.
825    ///
826    /// Due to limitations in the current implementation,
827    /// this will turn off rounding of the image.
828    pub rotation: Option<(Rot2, Vec2)>,
829
830    /// Round the corners of the image.
831    ///
832    /// The default is no rounding ([`CornerRadius::ZERO`]).
833    ///
834    /// Due to limitations in the current implementation,
835    /// this will turn off any rotation of the image.
836    pub corner_radius: CornerRadius,
837}
838
839impl Default for ImageOptions {
840    fn default() -> Self {
841        Self {
842            uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
843            bg_fill: Default::default(),
844            tint: Color32::WHITE,
845            rotation: None,
846            corner_radius: CornerRadius::ZERO,
847        }
848    }
849}
850
851pub fn paint_texture_at(
852    painter: &Painter,
853    rect: Rect,
854    options: &ImageOptions,
855    texture: &SizedTexture,
856) {
857    if options.bg_fill != Default::default() {
858        painter.add(RectShape::filled(
859            rect,
860            options.corner_radius,
861            options.bg_fill,
862        ));
863    }
864
865    match options.rotation {
866        Some((rot, origin)) => {
867            // TODO(emilk): implement this using `PathShape` (add texture support to it).
868            // This will also give us anti-aliasing of rotated images.
869            debug_assert!(
870                options.corner_radius == CornerRadius::ZERO,
871                "Image had both rounding and rotation. Please pick only one"
872            );
873
874            let mut mesh = Mesh::with_texture(texture.id);
875            mesh.add_rect_with_uv(rect, options.uv, options.tint);
876            mesh.rotate(rot, rect.min + origin * rect.size());
877            painter.add(Shape::mesh(mesh));
878        }
879        None => {
880            painter.add(
881                RectShape::filled(rect, options.corner_radius, options.tint)
882                    .with_texture(texture.id, options.uv),
883            );
884        }
885    }
886}
887
888#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
889/// Stores the durations between each frame of an animated image
890pub struct FrameDurations(Arc<Vec<Duration>>);
891
892impl FrameDurations {
893    pub fn new(durations: Vec<Duration>) -> Self {
894        Self(Arc::new(durations))
895    }
896
897    pub fn all(&self) -> Iter<'_, Duration> {
898        self.0.iter()
899    }
900}
901
902/// Animated image uris contain the uri & the frame that will be displayed
903fn encode_animated_image_uri(uri: &str, frame_index: usize) -> String {
904    format!("{uri}#{frame_index}")
905}
906
907/// Extracts uri and frame index
908/// # Errors
909/// Will return `Err` if `uri` does not match pattern {uri}-{frame_index}
910pub fn decode_animated_image_uri(uri: &str) -> Result<(&str, usize), String> {
911    let (uri, index) = uri
912        .rsplit_once('#')
913        .ok_or("Failed to find index separator '#'")?;
914    let index: usize = index.parse().map_err(|_err| {
915        format!("Failed to parse animated image frame index: {index:?} is not an integer")
916    })?;
917    Ok((uri, index))
918}
919
920/// Calculates at which frame the animated image is
921fn animated_image_frame_index(ctx: &Context, uri: &str) -> usize {
922    let now = ctx.input(|input| Duration::from_secs_f64(input.time));
923
924    let durations: Option<FrameDurations> = ctx.data(|data| data.get_temp(Id::new(uri)));
925
926    if let Some(durations) = durations {
927        let frames: Duration = durations.all().sum();
928        let pos_ms = now.as_millis() % frames.as_millis().max(1);
929
930        let mut cumulative_ms = 0;
931
932        for (index, duration) in durations.all().enumerate() {
933            cumulative_ms += duration.as_millis();
934
935            if pos_ms < cumulative_ms {
936                let ms_until_next_frame = cumulative_ms - pos_ms;
937                ctx.request_repaint_after(Duration::from_millis(ms_until_next_frame as u64));
938                return index;
939            }
940        }
941
942        0
943    } else {
944        0
945    }
946}
947
948/// Checks if uri is a gif file
949fn is_gif_uri(uri: &str) -> bool {
950    uri.ends_with(".gif") || uri.contains(".gif#")
951}
952
953/// Checks if bytes are gifs
954pub fn has_gif_magic_header(bytes: &[u8]) -> bool {
955    bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a")
956}
957
958/// Checks if uri is a webp file
959fn is_webp_uri(uri: &str) -> bool {
960    uri.ends_with(".webp") || uri.contains(".webp#")
961}
962
963/// Checks if bytes are webp
964pub fn has_webp_header(bytes: &[u8]) -> bool {
965    bytes.len() >= 12 && &bytes[0..4] == b"RIFF" && &bytes[8..12] == b"WEBP"
966}
967
968fn is_animated_image_uri(uri: &str) -> bool {
969    is_gif_uri(uri) || is_webp_uri(uri)
970}
971
972fn are_animated_image_bytes(bytes: &[u8]) -> bool {
973    has_gif_magic_header(bytes) || has_webp_header(bytes)
974}