egui/widgets/
image.rs

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