Skip to main content

egui/widgets/
image.rs

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