1use std::{borrow::Cow, slice::Iter, sync::Arc, time::Duration};
2
3use emath::{Align, Float as _, GuiRounding as _, NumExt as _, Rot2};
4use epaint::{
5 RectShape,
6 text::{LayoutJob, TextFormat, TextWrapping},
7};
8
9use crate::{
10 Color32, Context, CornerRadius, Id, Mesh, Painter, Rect, Response, Sense, Shape, Spinner,
11 TextStyle, TextureOptions, Ui, Vec2, Widget, WidgetInfo, WidgetType,
12 load::{Bytes, SizeHint, SizedTexture, TextureLoadResult, TexturePoll},
13 pos2,
14};
15
16#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
50#[derive(Debug, Clone)]
51pub struct Image<'a> {
52 source: ImageSource<'a>,
53 texture_options: TextureOptions,
54 image_options: ImageOptions,
55 sense: Sense,
56 size: ImageSize,
57 pub(crate) show_loading_spinner: Option<bool>,
58 pub(crate) alt_text: Option<String>,
59}
60
61impl<'a> Image<'a> {
62 pub fn new(source: impl Into<ImageSource<'a>>) -> Self {
64 fn new_mono(source: ImageSource<'_>) -> Image<'_> {
65 let size = if let ImageSource::Texture(tex) = &source {
66 ImageSize {
69 maintain_aspect_ratio: true,
70 max_size: Vec2::INFINITY,
71 fit: ImageFit::Exact(tex.size),
72 }
73 } else {
74 Default::default()
75 };
76
77 Image {
78 source,
79 texture_options: Default::default(),
80 image_options: Default::default(),
81 sense: Sense::hover(),
82 size,
83 show_loading_spinner: None,
84 alt_text: None,
85 }
86 }
87
88 new_mono(source.into())
89 }
90
91 pub fn from_uri(uri: impl Into<Cow<'a, str>>) -> Self {
95 Self::new(ImageSource::Uri(uri.into()))
96 }
97
98 pub fn from_texture(texture: impl Into<SizedTexture>) -> Self {
102 Self::new(ImageSource::Texture(texture.into()))
103 }
104
105 pub fn from_bytes(uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) -> Self {
111 Self::new(ImageSource::Bytes {
112 uri: uri.into(),
113 bytes: bytes.into(),
114 })
115 }
116
117 #[inline]
119 pub fn texture_options(mut self, texture_options: TextureOptions) -> Self {
120 self.texture_options = texture_options;
121 self
122 }
123
124 #[inline]
128 pub fn max_width(mut self, width: f32) -> Self {
129 self.size.max_size.x = width;
130 self
131 }
132
133 #[inline]
137 pub fn max_height(mut self, height: f32) -> Self {
138 self.size.max_size.y = height;
139 self
140 }
141
142 #[inline]
146 pub fn max_size(mut self, size: Vec2) -> Self {
147 self.size.max_size = size;
148 self
149 }
150
151 #[inline]
153 pub fn maintain_aspect_ratio(mut self, value: bool) -> Self {
154 self.size.maintain_aspect_ratio = value;
155 self
156 }
157
158 #[inline]
167 pub fn fit_to_original_size(mut self, scale: f32) -> Self {
168 self.size.fit = ImageFit::Original { scale };
169 self
170 }
171
172 #[inline]
176 pub fn fit_to_exact_size(mut self, size: Vec2) -> Self {
177 self.size.fit = ImageFit::Exact(size);
178 self
179 }
180
181 #[inline]
185 pub fn fit_to_fraction(mut self, fraction: Vec2) -> Self {
186 self.size.fit = ImageFit::Fraction(fraction);
187 self
188 }
189
190 #[inline]
196 pub fn shrink_to_fit(self) -> Self {
197 self.fit_to_fraction(Vec2::new(1.0, 1.0))
198 }
199
200 #[inline]
202 pub fn sense(mut self, sense: Sense) -> Self {
203 self.sense = sense;
204 self
205 }
206
207 #[inline]
209 pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
210 self.image_options.uv = uv.into();
211 self
212 }
213
214 #[inline]
216 pub fn bg_fill(mut self, bg_fill: impl Into<Color32>) -> Self {
217 self.image_options.bg_fill = bg_fill.into();
218 self
219 }
220
221 #[inline]
223 pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
224 self.image_options.tint = tint.into();
225 self
226 }
227
228 #[inline]
238 pub fn rotate(mut self, angle: f32, origin: Vec2) -> Self {
239 self.image_options.rotation = Some((Rot2::from_angle(angle), origin));
240 self.image_options.corner_radius = CornerRadius::ZERO; self
242 }
243
244 #[inline]
251 pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
252 self.image_options.corner_radius = corner_radius.into();
253 if self.image_options.corner_radius != CornerRadius::ZERO {
254 self.image_options.rotation = None; }
256 self
257 }
258
259 #[inline]
266 #[deprecated = "Renamed to `corner_radius`"]
267 pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
268 self.corner_radius(corner_radius)
269 }
270
271 #[inline]
275 pub fn show_loading_spinner(mut self, show: bool) -> Self {
276 self.show_loading_spinner = Some(show);
277 self
278 }
279
280 #[inline]
284 pub fn alt_text(mut self, label: impl Into<String>) -> Self {
285 self.alt_text = Some(label.into());
286 self
287 }
288}
289
290impl<'a, T: Into<ImageSource<'a>>> From<T> for Image<'a> {
291 fn from(value: T) -> Self {
292 Image::new(value)
293 }
294}
295
296impl<'a> Image<'a> {
297 #[inline]
299 pub fn calc_size(&self, available_size: Vec2, image_source_size: Option<Vec2>) -> Vec2 {
300 let image_source_size = image_source_size.unwrap_or(Vec2::splat(24.0)); self.size.calc_size(available_size, image_source_size)
302 }
303
304 pub fn load_and_calc_size(&self, ui: &Ui, available_size: Vec2) -> Option<Vec2> {
305 let image_size = self.load_for_size(ui.ctx(), available_size).ok()?.size()?;
306 Some(self.size.calc_size(available_size, image_size))
307 }
308
309 #[inline]
310 pub fn size(&self) -> Option<Vec2> {
311 match &self.source {
312 ImageSource::Texture(texture) => Some(texture.size),
313 ImageSource::Uri(_) | ImageSource::Bytes { .. } => None,
314 }
315 }
316
317 #[inline]
321 pub fn uri(&self) -> Option<&str> {
322 let uri = self.source.uri()?;
323
324 if let Ok((gif_uri, _index)) = decode_animated_image_uri(uri) {
325 Some(gif_uri)
326 } else {
327 Some(uri)
328 }
329 }
330
331 #[inline]
332 pub fn image_options(&self) -> &ImageOptions {
333 &self.image_options
334 }
335
336 #[inline]
337 pub fn source(&'a self, ctx: &Context) -> ImageSource<'a> {
338 match &self.source {
339 ImageSource::Uri(uri) if is_animated_image_uri(uri) => {
340 let frame_uri =
341 encode_animated_image_uri(uri, animated_image_frame_index(ctx, uri));
342 ImageSource::Uri(Cow::Owned(frame_uri))
343 }
344
345 ImageSource::Bytes { uri, bytes } if are_animated_image_bytes(bytes) => {
346 let frame_uri =
347 encode_animated_image_uri(uri, animated_image_frame_index(ctx, uri));
348 ctx.include_bytes(uri.clone(), bytes.clone());
349 ImageSource::Uri(Cow::Owned(frame_uri))
350 }
351 _ => self.source.clone(),
352 }
353 }
354
355 pub fn load_for_size(&self, ctx: &Context, available_size: Vec2) -> TextureLoadResult {
362 let size_hint = self.size.hint(available_size, ctx.pixels_per_point());
363 self.source(ctx)
364 .clone()
365 .load(ctx, self.texture_options, size_hint)
366 }
367
368 #[inline]
380 pub fn paint_at(&self, ui: &Ui, rect: Rect) {
381 let pixels_per_point = ui.pixels_per_point();
382
383 let rect = rect.round_to_pixels(pixels_per_point);
384
385 let pixel_size = (pixels_per_point * rect.size()).round();
388
389 let texture = self.source(ui.ctx()).clone().load(
390 ui.ctx(),
391 self.texture_options,
392 SizeHint::Size {
393 width: pixel_size.x as _,
394 height: pixel_size.y as _,
395 maintain_aspect_ratio: false, },
397 );
398
399 paint_texture_load_result(
400 ui,
401 &texture,
402 rect,
403 self.show_loading_spinner,
404 &self.image_options,
405 self.alt_text.as_deref(),
406 );
407 }
408}
409
410impl Widget for Image<'_> {
411 fn ui(self, ui: &mut Ui) -> Response {
412 let tlr = self.load_for_size(ui.ctx(), ui.available_size());
413 let image_source_size = tlr.as_ref().ok().and_then(|t| t.size());
414 let ui_size = self.calc_size(ui.available_size(), image_source_size);
415
416 let (rect, response) = ui.allocate_exact_size(ui_size, self.sense);
417 response.widget_info(|| {
418 let mut info = WidgetInfo::new(WidgetType::Image);
419 info.label = self.alt_text.clone();
420 info
421 });
422 if ui.is_rect_visible(rect) {
423 paint_texture_load_result(
424 ui,
425 &tlr,
426 rect,
427 self.show_loading_spinner,
428 &self.image_options,
429 self.alt_text.as_deref(),
430 );
431 }
432 texture_load_result_response(&self.source(ui.ctx()), &tlr, response)
433 }
434}
435
436#[derive(Debug, Clone, Copy)]
439pub struct ImageSize {
440 pub maintain_aspect_ratio: bool,
446
447 pub max_size: Vec2,
451
452 pub fit: ImageFit,
458}
459
460#[derive(Debug, Clone, Copy)]
464#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
465pub enum ImageFit {
466 Original { scale: f32 },
473
474 Fraction(Vec2),
476
477 Exact(Vec2),
481}
482
483impl ImageFit {
484 pub fn resolve(self, available_size: Vec2, image_size: Vec2) -> Vec2 {
485 match self {
486 Self::Original { scale } => image_size * scale,
487 Self::Fraction(fract) => available_size * fract,
488 Self::Exact(size) => size,
489 }
490 }
491}
492
493impl ImageSize {
494 pub fn hint(&self, available_size: Vec2, pixels_per_point: f32) -> SizeHint {
496 let Self {
497 maintain_aspect_ratio,
498 max_size,
499 fit,
500 } = *self;
501
502 let point_size = match fit {
503 ImageFit::Original { scale } => {
504 return SizeHint::Scale((pixels_per_point * scale).ord());
505 }
506 ImageFit::Fraction(fract) => available_size * fract,
507 ImageFit::Exact(size) => size,
508 };
509 let point_size = point_size.at_most(max_size);
510
511 let pixel_size = pixels_per_point * point_size;
512
513 match (pixel_size.x.is_finite(), pixel_size.y.is_finite()) {
515 (true, true) => SizeHint::Size {
516 width: pixel_size.x.round() as u32,
517 height: pixel_size.y.round() as u32,
518 maintain_aspect_ratio,
519 },
520 (true, false) => SizeHint::Width(pixel_size.x.round() as u32),
521 (false, true) => SizeHint::Height(pixel_size.y.round() as u32),
522 (false, false) => SizeHint::Scale(pixels_per_point.ord()),
523 }
524 }
525
526 pub fn calc_size(&self, available_size: Vec2, image_source_size: Vec2) -> Vec2 {
528 let Self {
529 maintain_aspect_ratio,
530 max_size,
531 fit,
532 } = *self;
533 match fit {
534 ImageFit::Original { scale } => {
535 let image_size = scale * image_source_size;
536 if image_size.x <= max_size.x && image_size.y <= max_size.y {
537 image_size
538 } else {
539 scale_to_fit(image_size, max_size, maintain_aspect_ratio)
540 }
541 }
542 ImageFit::Fraction(fract) => {
543 let scale_to_size = (available_size * fract).min(max_size);
544 scale_to_fit(image_source_size, scale_to_size, maintain_aspect_ratio)
545 }
546 ImageFit::Exact(size) => {
547 let scale_to_size = size.min(max_size);
548 scale_to_fit(image_source_size, scale_to_size, maintain_aspect_ratio)
549 }
550 }
551 }
552}
553
554fn scale_to_fit(image_size: Vec2, available_size: Vec2, maintain_aspect_ratio: bool) -> Vec2 {
556 if maintain_aspect_ratio {
557 let ratio_x = available_size.x / image_size.x;
558 let ratio_y = available_size.y / image_size.y;
559 let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y };
560 let ratio = if ratio.is_finite() { ratio } else { 1.0 };
561 image_size * ratio
562 } else {
563 available_size
564 }
565}
566
567impl Default for ImageSize {
568 #[inline]
569 fn default() -> Self {
570 Self {
571 max_size: Vec2::INFINITY,
572 fit: ImageFit::Fraction(Vec2::new(1.0, 1.0)),
573 maintain_aspect_ratio: true,
574 }
575 }
576}
577
578#[derive(Clone)]
582pub enum ImageSource<'a> {
583 Uri(Cow<'a, str>),
592
593 Texture(SizedTexture),
598
599 Bytes {
611 uri: Cow<'static, str>,
617
618 bytes: Bytes,
619 },
620}
621
622impl std::fmt::Debug for ImageSource<'_> {
623 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
624 match self {
625 ImageSource::Bytes { uri, .. } | ImageSource::Uri(uri) => uri.as_ref().fmt(f),
626 ImageSource::Texture(st) => st.id.fmt(f),
627 }
628 }
629}
630
631impl ImageSource<'_> {
632 #[inline]
634 pub fn texture_size(&self) -> Option<Vec2> {
635 match self {
636 ImageSource::Texture(texture) => Some(texture.size),
637 ImageSource::Uri(_) | ImageSource::Bytes { .. } => None,
638 }
639 }
640
641 pub fn load(
644 self,
645 ctx: &Context,
646 texture_options: TextureOptions,
647 size_hint: SizeHint,
648 ) -> TextureLoadResult {
649 match self {
650 Self::Texture(texture) => Ok(TexturePoll::Ready { texture }),
651 Self::Uri(uri) => ctx.try_load_texture(uri.as_ref(), texture_options, size_hint),
652 Self::Bytes { uri, bytes } => {
653 ctx.include_bytes(uri.clone(), bytes);
654 ctx.try_load_texture(uri.as_ref(), texture_options, size_hint)
655 }
656 }
657 }
658
659 pub fn uri(&self) -> Option<&str> {
663 match self {
664 ImageSource::Bytes { uri, .. } | ImageSource::Uri(uri) => Some(uri),
665 ImageSource::Texture(_) => None,
666 }
667 }
668}
669
670pub fn paint_texture_load_result(
671 ui: &Ui,
672 tlr: &TextureLoadResult,
673 rect: Rect,
674 show_loading_spinner: Option<bool>,
675 options: &ImageOptions,
676 alt_text: Option<&str>,
677) {
678 match tlr {
679 Ok(TexturePoll::Ready { texture }) => {
680 paint_texture_at(ui.painter(), rect, options, texture);
681 }
682 Ok(TexturePoll::Pending { .. }) => {
683 let show_loading_spinner =
684 show_loading_spinner.unwrap_or(ui.visuals().image_loading_spinners);
685 if show_loading_spinner {
686 Spinner::new().paint_at(ui, rect);
687 }
688 }
689 Err(_) => {
690 let font_id = TextStyle::Body.resolve(ui.style());
691 let mut job = LayoutJob {
692 wrap: TextWrapping::truncate_at_width(rect.width()),
693 halign: Align::Center,
694 ..Default::default()
695 };
696 job.append(
697 "⚠",
698 0.0,
699 TextFormat::simple(font_id.clone(), ui.visuals().error_fg_color),
700 );
701 if let Some(alt_text) = alt_text {
702 job.append(
703 alt_text,
704 ui.spacing().item_spacing.x,
705 TextFormat::simple(font_id, ui.visuals().text_color()),
706 );
707 }
708 let galley = ui.painter().layout_job(job);
709 ui.painter().galley(
710 rect.center() - Vec2::Y * galley.size().y * 0.5,
711 galley,
712 ui.visuals().text_color(),
713 );
714 }
715 }
716}
717
718pub fn texture_load_result_response(
720 source: &ImageSource<'_>,
721 tlr: &TextureLoadResult,
722 response: Response,
723) -> Response {
724 match tlr {
725 Ok(TexturePoll::Ready { .. }) => response,
726 Ok(TexturePoll::Pending { .. }) => {
727 let uri = source.uri().unwrap_or("image");
728 response.on_hover_text(format!("Loading {uri}…"))
729 }
730 Err(err) => {
731 let uri = source.uri().unwrap_or("image");
732 response.on_hover_text(format!("Failed loading {uri}: {err}"))
733 }
734 }
735}
736
737impl<'a> From<&'a str> for ImageSource<'a> {
738 #[inline]
739 fn from(value: &'a str) -> Self {
740 Self::Uri(value.into())
741 }
742}
743
744impl<'a> From<&'a String> for ImageSource<'a> {
745 #[inline]
746 fn from(value: &'a String) -> Self {
747 Self::Uri(value.as_str().into())
748 }
749}
750
751impl From<String> for ImageSource<'static> {
752 fn from(value: String) -> Self {
753 Self::Uri(value.into())
754 }
755}
756
757impl<'a> From<&'a Cow<'a, str>> for ImageSource<'a> {
758 #[inline]
759 fn from(value: &'a Cow<'a, str>) -> Self {
760 Self::Uri(value.clone())
761 }
762}
763
764impl<'a> From<Cow<'a, str>> for ImageSource<'a> {
765 #[inline]
766 fn from(value: Cow<'a, str>) -> Self {
767 Self::Uri(value)
768 }
769}
770
771impl<T: Into<Bytes>> From<(&'static str, T)> for ImageSource<'static> {
772 #[inline]
773 fn from((uri, bytes): (&'static str, T)) -> Self {
774 Self::Bytes {
775 uri: uri.into(),
776 bytes: bytes.into(),
777 }
778 }
779}
780
781impl<T: Into<Bytes>> From<(Cow<'static, str>, T)> for ImageSource<'static> {
782 #[inline]
783 fn from((uri, bytes): (Cow<'static, str>, T)) -> Self {
784 Self::Bytes {
785 uri,
786 bytes: bytes.into(),
787 }
788 }
789}
790
791impl<T: Into<Bytes>> From<(String, T)> for ImageSource<'static> {
792 #[inline]
793 fn from((uri, bytes): (String, T)) -> Self {
794 Self::Bytes {
795 uri: uri.into(),
796 bytes: bytes.into(),
797 }
798 }
799}
800
801impl<T: Into<SizedTexture>> From<T> for ImageSource<'static> {
802 fn from(value: T) -> Self {
803 Self::Texture(value.into())
804 }
805}
806
807#[derive(Debug, Clone)]
808#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
809pub struct ImageOptions {
810 pub uv: Rect,
812
813 pub bg_fill: Color32,
815
816 pub tint: Color32,
818
819 pub rotation: Option<(Rot2, Vec2)>,
829
830 pub corner_radius: CornerRadius,
837}
838
839impl Default for ImageOptions {
840 fn default() -> Self {
841 Self {
842 uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
843 bg_fill: Default::default(),
844 tint: Color32::WHITE,
845 rotation: None,
846 corner_radius: CornerRadius::ZERO,
847 }
848 }
849}
850
851pub fn paint_texture_at(
852 painter: &Painter,
853 rect: Rect,
854 options: &ImageOptions,
855 texture: &SizedTexture,
856) {
857 if options.bg_fill != Default::default() {
858 painter.add(RectShape::filled(
859 rect,
860 options.corner_radius,
861 options.bg_fill,
862 ));
863 }
864
865 match options.rotation {
866 Some((rot, origin)) => {
867 debug_assert!(
870 options.corner_radius == CornerRadius::ZERO,
871 "Image had both rounding and rotation. Please pick only one"
872 );
873
874 let mut mesh = Mesh::with_texture(texture.id);
875 mesh.add_rect_with_uv(rect, options.uv, options.tint);
876 mesh.rotate(rot, rect.min + origin * rect.size());
877 painter.add(Shape::mesh(mesh));
878 }
879 None => {
880 painter.add(
881 RectShape::filled(rect, options.corner_radius, options.tint)
882 .with_texture(texture.id, options.uv),
883 );
884 }
885 }
886}
887
888#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
889pub struct FrameDurations(Arc<Vec<Duration>>);
891
892impl FrameDurations {
893 pub fn new(durations: Vec<Duration>) -> Self {
894 Self(Arc::new(durations))
895 }
896
897 pub fn all(&self) -> Iter<'_, Duration> {
898 self.0.iter()
899 }
900}
901
902fn encode_animated_image_uri(uri: &str, frame_index: usize) -> String {
904 format!("{uri}#{frame_index}")
905}
906
907pub fn decode_animated_image_uri(uri: &str) -> Result<(&str, usize), String> {
911 let (uri, index) = uri
912 .rsplit_once('#')
913 .ok_or("Failed to find index separator '#'")?;
914 let index: usize = index.parse().map_err(|_err| {
915 format!("Failed to parse animated image frame index: {index:?} is not an integer")
916 })?;
917 Ok((uri, index))
918}
919
920fn animated_image_frame_index(ctx: &Context, uri: &str) -> usize {
922 let now = ctx.input(|input| Duration::from_secs_f64(input.time));
923
924 let durations: Option<FrameDurations> = ctx.data(|data| data.get_temp(Id::new(uri)));
925
926 if let Some(durations) = durations {
927 let frames: Duration = durations.all().sum();
928 let pos_ms = now.as_millis() % frames.as_millis().max(1);
929
930 let mut cumulative_ms = 0;
931
932 for (index, duration) in durations.all().enumerate() {
933 cumulative_ms += duration.as_millis();
934
935 if pos_ms < cumulative_ms {
936 let ms_until_next_frame = cumulative_ms - pos_ms;
937 ctx.request_repaint_after(Duration::from_millis(ms_until_next_frame as u64));
938 return index;
939 }
940 }
941
942 0
943 } else {
944 0
945 }
946}
947
948fn is_gif_uri(uri: &str) -> bool {
950 uri.ends_with(".gif") || uri.contains(".gif#")
951}
952
953pub fn has_gif_magic_header(bytes: &[u8]) -> bool {
955 bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a")
956}
957
958fn is_webp_uri(uri: &str) -> bool {
960 uri.ends_with(".webp") || uri.contains(".webp#")
961}
962
963pub fn has_webp_header(bytes: &[u8]) -> bool {
965 bytes.len() >= 12 && &bytes[0..4] == b"RIFF" && &bytes[8..12] == b"WEBP"
966}
967
968fn is_animated_image_uri(uri: &str) -> bool {
969 is_gif_uri(uri) || is_webp_uri(uri)
970}
971
972fn are_animated_image_bytes(bytes: &[u8]) -> bool {
973 has_gif_magic_header(bytes) || has_webp_header(bytes)
974}