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#[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 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 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 pub fn from_uri(uri: impl Into<Cow<'a, str>>) -> Self {
96 Self::new(ImageSource::Uri(uri.into()))
97 }
98
99 pub fn from_texture(texture: impl Into<SizedTexture>) -> Self {
103 Self::new(ImageSource::Texture(texture.into()))
104 }
105
106 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 #[inline]
120 pub fn texture_options(mut self, texture_options: TextureOptions) -> Self {
121 self.texture_options = texture_options;
122 self
123 }
124
125 #[inline]
129 pub fn max_width(mut self, width: f32) -> Self {
130 self.size.max_size.x = width;
131 self
132 }
133
134 #[inline]
138 pub fn max_height(mut self, height: f32) -> Self {
139 self.size.max_size.y = height;
140 self
141 }
142
143 #[inline]
147 pub fn max_size(mut self, size: Vec2) -> Self {
148 self.size.max_size = size;
149 self
150 }
151
152 #[inline]
154 pub fn maintain_aspect_ratio(mut self, value: bool) -> Self {
155 self.size.maintain_aspect_ratio = value;
156 self
157 }
158
159 #[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 #[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 #[inline]
186 pub fn fit_to_fraction(mut self, fraction: Vec2) -> Self {
187 self.size.fit = ImageFit::Fraction(fraction);
188 self
189 }
190
191 #[inline]
197 pub fn shrink_to_fit(self) -> Self {
198 self.fit_to_fraction(Vec2::new(1.0, 1.0))
199 }
200
201 #[inline]
203 pub fn sense(mut self, sense: Sense) -> Self {
204 self.sense = sense;
205 self
206 }
207
208 #[inline]
210 pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
211 self.image_options.uv = uv.into();
212 self
213 }
214
215 #[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 #[inline]
224 pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
225 self.image_options.tint = tint.into();
226 self
227 }
228
229 #[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; self
243 }
244
245 #[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; }
257 self
258 }
259
260 #[inline]
264 pub fn show_loading_spinner(mut self, show: bool) -> Self {
265 self.show_loading_spinner = Some(show);
266 self
267 }
268
269 #[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 #[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)); 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 #[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 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 #[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 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, },
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#[derive(Debug, Clone, Copy)]
428pub struct ImageSize {
429 pub maintain_aspect_ratio: bool,
435
436 pub max_size: Vec2,
440
441 pub fit: ImageFit,
447}
448
449#[derive(Debug, Clone, Copy)]
453#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
454pub enum ImageFit {
455 Original { scale: f32 },
462
463 Fraction(Vec2),
465
466 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 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 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 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
543fn 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#[derive(Clone)]
571pub enum ImageSource<'a> {
572 Uri(Cow<'a, str>),
581
582 Texture(SizedTexture),
587
588 Bytes {
600 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 #[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 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 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
707pub 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 pub uv: Rect,
801
802 pub bg_fill: Color32,
804
805 pub tint: Color32,
807
808 pub rotation: Option<(Rot2, Vec2)>,
818
819 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 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)]
878pub 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
891fn encode_animated_image_uri(uri: &str, frame_index: usize) -> String {
893 format!("{uri}#{frame_index}")
894}
895
896pub 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
909fn 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
935fn is_gif_uri(uri: &str) -> bool {
937 crate::load::has_extension(uri, "gif")
938}
939
940pub fn has_gif_magic_header(bytes: &[u8]) -> bool {
942 bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a")
943}
944
945fn is_webp_uri(uri: &str) -> bool {
947 crate::load::has_extension(uri, "webp")
948}
949
950pub 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}