1use std::{borrow::Cow, slice::Iter, sync::Arc, time::Duration};
2
3use emath::{Align, Float as _, Rot2};
4use epaint::{
5 text::{LayoutJob, TextFormat, TextWrapping},
6 RectShape,
7};
8
9use crate::{
10 load::{Bytes, SizeHint, SizedTexture, TextureLoadResult, TexturePoll},
11 pos2, Color32, Context, CornerRadius, Id, Mesh, Painter, Rect, Response, Sense, Shape, Spinner,
12 TextStyle, TextureOptions, Ui, Vec2, Widget, WidgetInfo, WidgetType,
13};
14
15#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
49#[derive(Debug, Clone)]
50pub struct Image<'a> {
51 source: ImageSource<'a>,
52 texture_options: TextureOptions,
53 image_options: ImageOptions,
54 sense: Sense,
55 size: ImageSize,
56 pub(crate) show_loading_spinner: Option<bool>,
57 alt_text: Option<String>,
58}
59
60impl<'a> Image<'a> {
61 pub fn new(source: impl Into<ImageSource<'a>>) -> Self {
63 fn new_mono(source: ImageSource<'_>) -> Image<'_> {
64 let size = if let ImageSource::Texture(tex) = &source {
65 ImageSize {
68 maintain_aspect_ratio: true,
69 max_size: Vec2::INFINITY,
70 fit: ImageFit::Exact(tex.size),
71 }
72 } else {
73 Default::default()
74 };
75
76 Image {
77 source,
78 texture_options: Default::default(),
79 image_options: Default::default(),
80 sense: Sense::hover(),
81 size,
82 show_loading_spinner: None,
83 alt_text: None,
84 }
85 }
86
87 new_mono(source.into())
88 }
89
90 pub fn from_uri(uri: impl Into<Cow<'a, str>>) -> Self {
94 Self::new(ImageSource::Uri(uri.into()))
95 }
96
97 pub fn from_texture(texture: impl Into<SizedTexture>) -> Self {
101 Self::new(ImageSource::Texture(texture.into()))
102 }
103
104 pub fn from_bytes(uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) -> Self {
110 Self::new(ImageSource::Bytes {
111 uri: uri.into(),
112 bytes: bytes.into(),
113 })
114 }
115
116 #[inline]
118 pub fn texture_options(mut self, texture_options: TextureOptions) -> Self {
119 self.texture_options = texture_options;
120 self
121 }
122
123 #[inline]
127 pub fn max_width(mut self, width: f32) -> Self {
128 self.size.max_size.x = width;
129 self
130 }
131
132 #[inline]
136 pub fn max_height(mut self, height: f32) -> Self {
137 self.size.max_size.y = height;
138 self
139 }
140
141 #[inline]
145 pub fn max_size(mut self, size: Vec2) -> Self {
146 self.size.max_size = size;
147 self
148 }
149
150 #[inline]
152 pub fn maintain_aspect_ratio(mut self, value: bool) -> Self {
153 self.size.maintain_aspect_ratio = value;
154 self
155 }
156
157 #[inline]
163 pub fn fit_to_original_size(mut self, scale: f32) -> Self {
164 self.size.fit = ImageFit::Original { scale };
165 self
166 }
167
168 #[inline]
172 pub fn fit_to_exact_size(mut self, size: Vec2) -> Self {
173 self.size.fit = ImageFit::Exact(size);
174 self
175 }
176
177 #[inline]
181 pub fn fit_to_fraction(mut self, fraction: Vec2) -> Self {
182 self.size.fit = ImageFit::Fraction(fraction);
183 self
184 }
185
186 #[inline]
192 pub fn shrink_to_fit(self) -> Self {
193 self.fit_to_fraction(Vec2::new(1.0, 1.0))
194 }
195
196 #[inline]
198 pub fn sense(mut self, sense: Sense) -> Self {
199 self.sense = sense;
200 self
201 }
202
203 #[inline]
205 pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
206 self.image_options.uv = uv.into();
207 self
208 }
209
210 #[inline]
212 pub fn bg_fill(mut self, bg_fill: impl Into<Color32>) -> Self {
213 self.image_options.bg_fill = bg_fill.into();
214 self
215 }
216
217 #[inline]
219 pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
220 self.image_options.tint = tint.into();
221 self
222 }
223
224 #[inline]
234 pub fn rotate(mut self, angle: f32, origin: Vec2) -> Self {
235 self.image_options.rotation = Some((Rot2::from_angle(angle), origin));
236 self.image_options.corner_radius = CornerRadius::ZERO; self
238 }
239
240 #[inline]
247 pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
248 self.image_options.corner_radius = corner_radius.into();
249 if self.image_options.corner_radius != CornerRadius::ZERO {
250 self.image_options.rotation = None; }
252 self
253 }
254
255 #[inline]
262 #[deprecated = "Renamed to `corner_radius`"]
263 pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
264 self.corner_radius(corner_radius)
265 }
266
267 #[inline]
271 pub fn show_loading_spinner(mut self, show: bool) -> Self {
272 self.show_loading_spinner = Some(show);
273 self
274 }
275
276 #[inline]
279 pub fn alt_text(mut self, label: impl Into<String>) -> Self {
280 self.alt_text = Some(label.into());
281 self
282 }
283}
284
285impl<'a, T: Into<ImageSource<'a>>> From<T> for Image<'a> {
286 fn from(value: T) -> Self {
287 Image::new(value)
288 }
289}
290
291impl<'a> Image<'a> {
292 #[inline]
294 pub fn calc_size(&self, available_size: Vec2, original_image_size: Option<Vec2>) -> Vec2 {
295 let original_image_size = original_image_size.unwrap_or(Vec2::splat(24.0)); self.size.calc_size(available_size, original_image_size)
297 }
298
299 pub fn load_and_calc_size(&self, ui: &Ui, available_size: Vec2) -> Option<Vec2> {
300 let image_size = self.load_for_size(ui.ctx(), available_size).ok()?.size()?;
301 Some(self.size.calc_size(available_size, image_size))
302 }
303
304 #[inline]
305 pub fn size(&self) -> Option<Vec2> {
306 match &self.source {
307 ImageSource::Texture(texture) => Some(texture.size),
308 ImageSource::Uri(_) | ImageSource::Bytes { .. } => None,
309 }
310 }
311
312 #[inline]
316 pub fn uri(&self) -> Option<&str> {
317 let uri = self.source.uri()?;
318
319 if let Ok((gif_uri, _index)) = decode_animated_image_uri(uri) {
320 Some(gif_uri)
321 } else {
322 Some(uri)
323 }
324 }
325
326 #[inline]
327 pub fn image_options(&self) -> &ImageOptions {
328 &self.image_options
329 }
330
331 #[inline]
332 pub fn source(&'a self, ctx: &Context) -> ImageSource<'a> {
333 match &self.source {
334 ImageSource::Uri(uri) if is_animated_image_uri(uri) => {
335 let frame_uri =
336 encode_animated_image_uri(uri, animated_image_frame_index(ctx, uri));
337 ImageSource::Uri(Cow::Owned(frame_uri))
338 }
339
340 ImageSource::Bytes { uri, bytes } if are_animated_image_bytes(bytes) => {
341 let frame_uri =
342 encode_animated_image_uri(uri, animated_image_frame_index(ctx, uri));
343 ctx.include_bytes(uri.clone(), bytes.clone());
344 ImageSource::Uri(Cow::Owned(frame_uri))
345 }
346 _ => self.source.clone(),
347 }
348 }
349
350 pub fn load_for_size(&self, ctx: &Context, available_size: Vec2) -> TextureLoadResult {
357 let size_hint = self.size.hint(available_size, ctx.pixels_per_point());
358 self.source(ctx)
359 .clone()
360 .load(ctx, self.texture_options, size_hint)
361 }
362
363 #[inline]
375 pub fn paint_at(&self, ui: &Ui, rect: Rect) {
376 paint_texture_load_result(
377 ui,
378 &self.load_for_size(ui.ctx(), rect.size()),
379 rect,
380 self.show_loading_spinner,
381 &self.image_options,
382 self.alt_text.as_deref(),
383 );
384 }
385}
386
387impl Widget for Image<'_> {
388 fn ui(self, ui: &mut Ui) -> Response {
389 let tlr = self.load_for_size(ui.ctx(), ui.available_size());
390 let original_image_size = tlr.as_ref().ok().and_then(|t| t.size());
391 let ui_size = self.calc_size(ui.available_size(), original_image_size);
392
393 let (rect, response) = ui.allocate_exact_size(ui_size, self.sense);
394 response.widget_info(|| {
395 let mut info = WidgetInfo::new(WidgetType::Image);
396 info.label = self.alt_text.clone();
397 info
398 });
399 if ui.is_rect_visible(rect) {
400 paint_texture_load_result(
401 ui,
402 &tlr,
403 rect,
404 self.show_loading_spinner,
405 &self.image_options,
406 self.alt_text.as_deref(),
407 );
408 }
409 texture_load_result_response(&self.source(ui.ctx()), &tlr, response)
410 }
411}
412
413#[derive(Debug, Clone, Copy)]
416pub struct ImageSize {
417 pub maintain_aspect_ratio: bool,
423
424 pub max_size: Vec2,
428
429 pub fit: ImageFit,
435}
436
437#[derive(Debug, Clone, Copy)]
441#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
442pub enum ImageFit {
443 Original { scale: f32 },
447
448 Fraction(Vec2),
450
451 Exact(Vec2),
455}
456
457impl ImageFit {
458 pub fn resolve(self, available_size: Vec2, image_size: Vec2) -> Vec2 {
459 match self {
460 Self::Original { scale } => image_size * scale,
461 Self::Fraction(fract) => available_size * fract,
462 Self::Exact(size) => size,
463 }
464 }
465}
466
467impl ImageSize {
468 pub fn hint(&self, available_size: Vec2, pixels_per_point: f32) -> SizeHint {
470 let size = match self.fit {
471 ImageFit::Original { scale } => return SizeHint::Scale(scale.ord()),
472 ImageFit::Fraction(fract) => available_size * fract,
473 ImageFit::Exact(size) => size,
474 };
475 let size = size.min(self.max_size);
476 let size = size * pixels_per_point;
477
478 match (size.x.is_finite(), size.y.is_finite()) {
480 (true, true) => SizeHint::Size(size.x.round() as u32, size.y.round() as u32),
481 (true, false) => SizeHint::Width(size.x.round() as u32),
482 (false, true) => SizeHint::Height(size.y.round() as u32),
483 (false, false) => SizeHint::Scale(pixels_per_point.ord()),
484 }
485 }
486
487 pub fn calc_size(&self, available_size: Vec2, original_image_size: Vec2) -> Vec2 {
489 let Self {
490 maintain_aspect_ratio,
491 max_size,
492 fit,
493 } = *self;
494 match fit {
495 ImageFit::Original { scale } => {
496 let image_size = original_image_size * scale;
497 if image_size.x <= max_size.x && image_size.y <= max_size.y {
498 image_size
499 } else {
500 scale_to_fit(image_size, max_size, maintain_aspect_ratio)
501 }
502 }
503 ImageFit::Fraction(fract) => {
504 let scale_to_size = (available_size * fract).min(max_size);
505 scale_to_fit(original_image_size, scale_to_size, maintain_aspect_ratio)
506 }
507 ImageFit::Exact(size) => {
508 let scale_to_size = size.min(max_size);
509 scale_to_fit(original_image_size, scale_to_size, maintain_aspect_ratio)
510 }
511 }
512 }
513}
514
515fn scale_to_fit(image_size: Vec2, available_size: Vec2, maintain_aspect_ratio: bool) -> Vec2 {
517 if maintain_aspect_ratio {
518 let ratio_x = available_size.x / image_size.x;
519 let ratio_y = available_size.y / image_size.y;
520 let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y };
521 let ratio = if ratio.is_finite() { ratio } else { 1.0 };
522 image_size * ratio
523 } else {
524 available_size
525 }
526}
527
528impl Default for ImageSize {
529 #[inline]
530 fn default() -> Self {
531 Self {
532 max_size: Vec2::INFINITY,
533 fit: ImageFit::Fraction(Vec2::new(1.0, 1.0)),
534 maintain_aspect_ratio: true,
535 }
536 }
537}
538
539#[derive(Clone)]
543pub enum ImageSource<'a> {
544 Uri(Cow<'a, str>),
553
554 Texture(SizedTexture),
559
560 Bytes {
572 uri: Cow<'static, str>,
578
579 bytes: Bytes,
580 },
581}
582
583impl std::fmt::Debug for ImageSource<'_> {
584 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
585 match self {
586 ImageSource::Bytes { uri, .. } | ImageSource::Uri(uri) => uri.as_ref().fmt(f),
587 ImageSource::Texture(st) => st.id.fmt(f),
588 }
589 }
590}
591
592impl ImageSource<'_> {
593 #[inline]
595 pub fn texture_size(&self) -> Option<Vec2> {
596 match self {
597 ImageSource::Texture(texture) => Some(texture.size),
598 ImageSource::Uri(_) | ImageSource::Bytes { .. } => None,
599 }
600 }
601
602 pub fn load(
605 self,
606 ctx: &Context,
607 texture_options: TextureOptions,
608 size_hint: SizeHint,
609 ) -> TextureLoadResult {
610 match self {
611 Self::Texture(texture) => Ok(TexturePoll::Ready { texture }),
612 Self::Uri(uri) => ctx.try_load_texture(uri.as_ref(), texture_options, size_hint),
613 Self::Bytes { uri, bytes } => {
614 ctx.include_bytes(uri.clone(), bytes);
615 ctx.try_load_texture(uri.as_ref(), texture_options, size_hint)
616 }
617 }
618 }
619
620 pub fn uri(&self) -> Option<&str> {
624 match self {
625 ImageSource::Bytes { uri, .. } | ImageSource::Uri(uri) => Some(uri),
626 ImageSource::Texture(_) => None,
627 }
628 }
629}
630
631pub fn paint_texture_load_result(
632 ui: &Ui,
633 tlr: &TextureLoadResult,
634 rect: Rect,
635 show_loading_spinner: Option<bool>,
636 options: &ImageOptions,
637 alt: Option<&str>,
638) {
639 match tlr {
640 Ok(TexturePoll::Ready { texture }) => {
641 paint_texture_at(ui.painter(), rect, options, texture);
642 }
643 Ok(TexturePoll::Pending { .. }) => {
644 let show_loading_spinner =
645 show_loading_spinner.unwrap_or(ui.visuals().image_loading_spinners);
646 if show_loading_spinner {
647 Spinner::new().paint_at(ui, rect);
648 }
649 }
650 Err(_) => {
651 let font_id = TextStyle::Body.resolve(ui.style());
652 let mut job = LayoutJob {
653 wrap: TextWrapping::truncate_at_width(rect.width()),
654 halign: Align::Center,
655 ..Default::default()
656 };
657 job.append(
658 "⚠",
659 0.0,
660 TextFormat::simple(font_id.clone(), ui.visuals().error_fg_color),
661 );
662 if let Some(alt) = alt {
663 job.append(
664 alt,
665 ui.spacing().item_spacing.x,
666 TextFormat::simple(font_id, ui.visuals().text_color()),
667 );
668 }
669 let galley = ui.painter().layout_job(job);
670 ui.painter().galley(
671 rect.center() - Vec2::Y * galley.size().y * 0.5,
672 galley,
673 ui.visuals().text_color(),
674 );
675 }
676 }
677}
678
679pub fn texture_load_result_response(
681 source: &ImageSource<'_>,
682 tlr: &TextureLoadResult,
683 response: Response,
684) -> Response {
685 match tlr {
686 Ok(TexturePoll::Ready { .. }) => response,
687 Ok(TexturePoll::Pending { .. }) => {
688 let uri = source.uri().unwrap_or("image");
689 response.on_hover_text(format!("Loading {uri}…"))
690 }
691 Err(err) => {
692 let uri = source.uri().unwrap_or("image");
693 response.on_hover_text(format!("Failed loading {uri}: {err}"))
694 }
695 }
696}
697
698impl<'a> From<&'a str> for ImageSource<'a> {
699 #[inline]
700 fn from(value: &'a str) -> Self {
701 Self::Uri(value.into())
702 }
703}
704
705impl<'a> From<&'a String> for ImageSource<'a> {
706 #[inline]
707 fn from(value: &'a String) -> Self {
708 Self::Uri(value.as_str().into())
709 }
710}
711
712impl From<String> for ImageSource<'static> {
713 fn from(value: String) -> Self {
714 Self::Uri(value.into())
715 }
716}
717
718impl<'a> From<&'a Cow<'a, str>> for ImageSource<'a> {
719 #[inline]
720 fn from(value: &'a Cow<'a, str>) -> Self {
721 Self::Uri(value.clone())
722 }
723}
724
725impl<'a> From<Cow<'a, str>> for ImageSource<'a> {
726 #[inline]
727 fn from(value: Cow<'a, str>) -> Self {
728 Self::Uri(value)
729 }
730}
731
732impl<T: Into<Bytes>> From<(&'static str, T)> for ImageSource<'static> {
733 #[inline]
734 fn from((uri, bytes): (&'static str, T)) -> Self {
735 Self::Bytes {
736 uri: uri.into(),
737 bytes: bytes.into(),
738 }
739 }
740}
741
742impl<T: Into<Bytes>> From<(Cow<'static, str>, T)> for ImageSource<'static> {
743 #[inline]
744 fn from((uri, bytes): (Cow<'static, str>, T)) -> Self {
745 Self::Bytes {
746 uri,
747 bytes: bytes.into(),
748 }
749 }
750}
751
752impl<T: Into<Bytes>> From<(String, T)> for ImageSource<'static> {
753 #[inline]
754 fn from((uri, bytes): (String, T)) -> Self {
755 Self::Bytes {
756 uri: uri.into(),
757 bytes: bytes.into(),
758 }
759 }
760}
761
762impl<T: Into<SizedTexture>> From<T> for ImageSource<'static> {
763 fn from(value: T) -> Self {
764 Self::Texture(value.into())
765 }
766}
767
768#[derive(Debug, Clone)]
769#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
770pub struct ImageOptions {
771 pub uv: Rect,
773
774 pub bg_fill: Color32,
776
777 pub tint: Color32,
779
780 pub rotation: Option<(Rot2, Vec2)>,
790
791 pub corner_radius: CornerRadius,
798}
799
800impl Default for ImageOptions {
801 fn default() -> Self {
802 Self {
803 uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
804 bg_fill: Default::default(),
805 tint: Color32::WHITE,
806 rotation: None,
807 corner_radius: CornerRadius::ZERO,
808 }
809 }
810}
811
812pub fn paint_texture_at(
813 painter: &Painter,
814 rect: Rect,
815 options: &ImageOptions,
816 texture: &SizedTexture,
817) {
818 if options.bg_fill != Default::default() {
819 painter.add(RectShape::filled(
820 rect,
821 options.corner_radius,
822 options.bg_fill,
823 ));
824 }
825
826 match options.rotation {
827 Some((rot, origin)) => {
828 debug_assert!(
831 options.corner_radius == CornerRadius::ZERO,
832 "Image had both rounding and rotation. Please pick only one"
833 );
834
835 let mut mesh = Mesh::with_texture(texture.id);
836 mesh.add_rect_with_uv(rect, options.uv, options.tint);
837 mesh.rotate(rot, rect.min + origin * rect.size());
838 painter.add(Shape::mesh(mesh));
839 }
840 None => {
841 painter.add(
842 RectShape::filled(rect, options.corner_radius, options.tint)
843 .with_texture(texture.id, options.uv),
844 );
845 }
846 }
847}
848
849#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
850pub struct FrameDurations(Arc<Vec<Duration>>);
852
853impl FrameDurations {
854 pub fn new(durations: Vec<Duration>) -> Self {
855 Self(Arc::new(durations))
856 }
857
858 pub fn all(&self) -> Iter<'_, Duration> {
859 self.0.iter()
860 }
861}
862
863fn encode_animated_image_uri(uri: &str, frame_index: usize) -> String {
865 format!("{uri}#{frame_index}")
866}
867
868pub fn decode_animated_image_uri(uri: &str) -> Result<(&str, usize), String> {
872 let (uri, index) = uri
873 .rsplit_once('#')
874 .ok_or("Failed to find index separator '#'")?;
875 let index: usize = index.parse().map_err(|_err| {
876 format!("Failed to parse animated image frame index: {index:?} is not an integer")
877 })?;
878 Ok((uri, index))
879}
880
881fn animated_image_frame_index(ctx: &Context, uri: &str) -> usize {
883 let now = ctx.input(|input| Duration::from_secs_f64(input.time));
884
885 let durations: Option<FrameDurations> = ctx.data(|data| data.get_temp(Id::new(uri)));
886
887 if let Some(durations) = durations {
888 let frames: Duration = durations.all().sum();
889 let pos_ms = now.as_millis() % frames.as_millis().max(1);
890
891 let mut cumulative_ms = 0;
892
893 for (index, duration) in durations.all().enumerate() {
894 cumulative_ms += duration.as_millis();
895
896 if pos_ms < cumulative_ms {
897 let ms_until_next_frame = cumulative_ms - pos_ms;
898 ctx.request_repaint_after(Duration::from_millis(ms_until_next_frame as u64));
899 return index;
900 }
901 }
902
903 0
904 } else {
905 0
906 }
907}
908
909fn is_gif_uri(uri: &str) -> bool {
911 uri.ends_with(".gif") || uri.contains(".gif#")
912}
913
914pub fn has_gif_magic_header(bytes: &[u8]) -> bool {
916 bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a")
917}
918
919fn is_webp_uri(uri: &str) -> bool {
921 uri.ends_with(".webp") || uri.contains(".webp#")
922}
923
924pub fn has_webp_header(bytes: &[u8]) -> bool {
926 bytes.len() >= 12 && &bytes[0..4] == b"RIFF" && &bytes[8..12] == b"WEBP"
927}
928
929fn is_animated_image_uri(uri: &str) -> bool {
930 is_gif_uri(uri) || is_webp_uri(uri)
931}
932
933fn are_animated_image_bytes(bytes: &[u8]) -> bool {
934 has_gif_magic_header(bytes) || has_webp_header(bytes)
935}