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#[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 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 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 pub fn from_uri(uri: impl Into<Cow<'a, str>>) -> Self {
88 Self::new(ImageSource::Uri(uri.into()))
89 }
90
91 pub fn from_texture(texture: impl Into<SizedTexture>) -> Self {
95 Self::new(ImageSource::Texture(texture.into()))
96 }
97
98 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 #[inline]
112 pub fn texture_options(mut self, texture_options: TextureOptions) -> Self {
113 self.texture_options = texture_options;
114 self
115 }
116
117 #[inline]
121 pub fn max_width(mut self, width: f32) -> Self {
122 self.size.max_size.x = width;
123 self
124 }
125
126 #[inline]
130 pub fn max_height(mut self, height: f32) -> Self {
131 self.size.max_size.y = height;
132 self
133 }
134
135 #[inline]
139 pub fn max_size(mut self, size: Vec2) -> Self {
140 self.size.max_size = size;
141 self
142 }
143
144 #[inline]
146 pub fn maintain_aspect_ratio(mut self, value: bool) -> Self {
147 self.size.maintain_aspect_ratio = value;
148 self
149 }
150
151 #[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 #[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 #[inline]
175 pub fn fit_to_fraction(mut self, fraction: Vec2) -> Self {
176 self.size.fit = ImageFit::Fraction(fraction);
177 self
178 }
179
180 #[inline]
186 pub fn shrink_to_fit(self) -> Self {
187 self.fit_to_fraction(Vec2::new(1.0, 1.0))
188 }
189
190 #[inline]
192 pub fn sense(mut self, sense: Sense) -> Self {
193 self.sense = sense;
194 self
195 }
196
197 #[inline]
199 pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
200 self.image_options.uv = uv.into();
201 self
202 }
203
204 #[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 #[inline]
213 pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
214 self.image_options.tint = tint.into();
215 self
216 }
217
218 #[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; self
232 }
233
234 #[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; }
246 self
247 }
248
249 #[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 #[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)); 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 #[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 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 #[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#[derive(Debug, Clone, Copy)]
381pub struct ImageSize {
382 pub maintain_aspect_ratio: bool,
388
389 pub max_size: Vec2,
393
394 pub fit: ImageFit,
400}
401
402#[derive(Debug, Clone, Copy)]
406#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
407pub enum ImageFit {
408 Original { scale: f32 },
412
413 Fraction(Vec2),
415
416 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 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 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 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
482fn 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#[derive(Clone)]
510pub enum ImageSource<'a> {
511 Uri(Cow<'a, str>),
520
521 Texture(SizedTexture),
526
527 Bytes {
539 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 #[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 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 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
629pub 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 pub uv: Rect,
723
724 pub bg_fill: Color32,
726
727 pub tint: Color32,
729
730 pub rotation: Option<(Rot2, Vec2)>,
740
741 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 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
800fn encode_gif_uri(uri: &str, frame_index: usize) -> String {
802 format!("{uri}#{frame_index}")
803}
804
805pub 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
818fn is_gif_uri(uri: &str) -> bool {
820 uri.ends_with(".gif") || uri.contains(".gif#")
821}
822
823pub fn has_gif_magic_header(bytes: &[u8]) -> bool {
825 bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a")
826}
827
828fn 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)]
852pub struct GifFrameDurations(pub Arc<Vec<Duration>>);