egui/widgets/
image.rs

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