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 pos2, Align2, Color32, Context, Id, Mesh, Painter, Rect, Response, Rounding, Sense, Shape,
9 Spinner, Stroke, TextStyle, TextureOptions, Ui, Vec2, Widget,
10};
11
12#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
46#[derive(Debug, Clone)]
47pub struct Image<'a> {
48 source: ImageSource<'a>,
49 texture_options: TextureOptions,
50 image_options: ImageOptions,
51 sense: Sense,
52 size: ImageSize,
53 pub(crate) show_loading_spinner: Option<bool>,
54}
55
56impl<'a> Image<'a> {
57 pub fn new(source: impl Into<ImageSource<'a>>) -> Self {
59 fn new_mono(source: ImageSource<'_>) -> Image<'_> {
60 let size = if let ImageSource::Texture(tex) = &source {
61 ImageSize {
64 maintain_aspect_ratio: true,
65 max_size: Vec2::INFINITY,
66 fit: ImageFit::Exact(tex.size),
67 }
68 } else {
69 Default::default()
70 };
71
72 Image {
73 source,
74 texture_options: Default::default(),
75 image_options: Default::default(),
76 sense: Sense::hover(),
77 size,
78 show_loading_spinner: None,
79 }
80 }
81
82 new_mono(source.into())
83 }
84
85 pub fn from_uri(uri: impl Into<Cow<'a, str>>) -> Self {
89 Self::new(ImageSource::Uri(uri.into()))
90 }
91
92 pub fn from_texture(texture: impl Into<SizedTexture>) -> Self {
96 Self::new(ImageSource::Texture(texture.into()))
97 }
98
99 pub fn from_bytes(uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) -> Self {
105 Self::new(ImageSource::Bytes {
106 uri: uri.into(),
107 bytes: bytes.into(),
108 })
109 }
110
111 #[inline]
113 pub fn texture_options(mut self, texture_options: TextureOptions) -> Self {
114 self.texture_options = texture_options;
115 self
116 }
117
118 #[inline]
122 pub fn max_width(mut self, width: f32) -> Self {
123 self.size.max_size.x = width;
124 self
125 }
126
127 #[inline]
131 pub fn max_height(mut self, height: f32) -> Self {
132 self.size.max_size.y = height;
133 self
134 }
135
136 #[inline]
140 pub fn max_size(mut self, size: Vec2) -> Self {
141 self.size.max_size = size;
142 self
143 }
144
145 #[inline]
147 pub fn maintain_aspect_ratio(mut self, value: bool) -> Self {
148 self.size.maintain_aspect_ratio = value;
149 self
150 }
151
152 #[inline]
158 pub fn fit_to_original_size(mut self, scale: f32) -> Self {
159 self.size.fit = ImageFit::Original { scale };
160 self
161 }
162
163 #[inline]
167 pub fn fit_to_exact_size(mut self, size: Vec2) -> Self {
168 self.size.fit = ImageFit::Exact(size);
169 self
170 }
171
172 #[inline]
176 pub fn fit_to_fraction(mut self, fraction: Vec2) -> Self {
177 self.size.fit = ImageFit::Fraction(fraction);
178 self
179 }
180
181 #[inline]
187 pub fn shrink_to_fit(self) -> Self {
188 self.fit_to_fraction(Vec2::new(1.0, 1.0))
189 }
190
191 #[inline]
193 pub fn sense(mut self, sense: Sense) -> Self {
194 self.sense = sense;
195 self
196 }
197
198 #[inline]
200 pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
201 self.image_options.uv = uv.into();
202 self
203 }
204
205 #[inline]
207 pub fn bg_fill(mut self, bg_fill: impl Into<Color32>) -> Self {
208 self.image_options.bg_fill = bg_fill.into();
209 self
210 }
211
212 #[inline]
214 pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
215 self.image_options.tint = tint.into();
216 self
217 }
218
219 #[inline]
229 pub fn rotate(mut self, angle: f32, origin: Vec2) -> Self {
230 self.image_options.rotation = Some((Rot2::from_angle(angle), origin));
231 self.image_options.rounding = Rounding::ZERO; self
233 }
234
235 #[inline]
242 pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
243 self.image_options.rounding = rounding.into();
244 if self.image_options.rounding != Rounding::ZERO {
245 self.image_options.rotation = None; }
247 self
248 }
249
250 #[inline]
254 pub fn show_loading_spinner(mut self, show: bool) -> Self {
255 self.show_loading_spinner = Some(show);
256 self
257 }
258}
259
260impl<'a, T: Into<ImageSource<'a>>> From<T> for Image<'a> {
261 fn from(value: T) -> Self {
262 Image::new(value)
263 }
264}
265
266impl<'a> Image<'a> {
267 #[inline]
269 pub fn calc_size(&self, available_size: Vec2, original_image_size: Option<Vec2>) -> Vec2 {
270 let original_image_size = original_image_size.unwrap_or(Vec2::splat(24.0)); self.size.calc_size(available_size, original_image_size)
272 }
273
274 pub fn load_and_calc_size(&self, ui: &Ui, available_size: Vec2) -> Option<Vec2> {
275 let image_size = self.load_for_size(ui.ctx(), available_size).ok()?.size()?;
276 Some(self.size.calc_size(available_size, image_size))
277 }
278
279 #[inline]
280 pub fn size(&self) -> Option<Vec2> {
281 match &self.source {
282 ImageSource::Texture(texture) => Some(texture.size),
283 ImageSource::Uri(_) | ImageSource::Bytes { .. } => None,
284 }
285 }
286
287 #[inline]
291 pub fn uri(&self) -> Option<&str> {
292 let uri = self.source.uri()?;
293
294 if let Ok((gif_uri, _index)) = decode_gif_uri(uri) {
295 Some(gif_uri)
296 } else {
297 Some(uri)
298 }
299 }
300
301 #[inline]
302 pub fn image_options(&self) -> &ImageOptions {
303 &self.image_options
304 }
305
306 #[inline]
307 pub fn source(&'a self, ctx: &Context) -> ImageSource<'a> {
308 match &self.source {
309 ImageSource::Uri(uri) if is_gif_uri(uri) => {
310 let frame_uri = encode_gif_uri(uri, gif_frame_index(ctx, uri));
311 ImageSource::Uri(Cow::Owned(frame_uri))
312 }
313
314 ImageSource::Bytes { uri, bytes } if is_gif_uri(uri) || has_gif_magic_header(bytes) => {
315 let frame_uri = encode_gif_uri(uri, gif_frame_index(ctx, uri));
316 ctx.include_bytes(uri.clone(), bytes.clone());
317 ImageSource::Uri(Cow::Owned(frame_uri))
318 }
319 _ => self.source.clone(),
320 }
321 }
322
323 pub fn load_for_size(&self, ctx: &Context, available_size: Vec2) -> TextureLoadResult {
330 let size_hint = self.size.hint(available_size, ctx.pixels_per_point());
331 self.source(ctx)
332 .clone()
333 .load(ctx, self.texture_options, size_hint)
334 }
335
336 #[inline]
348 pub fn paint_at(&self, ui: &Ui, rect: Rect) {
349 paint_texture_load_result(
350 ui,
351 &self.load_for_size(ui.ctx(), rect.size()),
352 rect,
353 self.show_loading_spinner,
354 &self.image_options,
355 );
356 }
357}
358
359impl<'a> Widget for Image<'a> {
360 fn ui(self, ui: &mut Ui) -> Response {
361 let tlr = self.load_for_size(ui.ctx(), ui.available_size());
362 let original_image_size = tlr.as_ref().ok().and_then(|t| t.size());
363 let ui_size = self.calc_size(ui.available_size(), original_image_size);
364
365 let (rect, response) = ui.allocate_exact_size(ui_size, self.sense);
366 if ui.is_rect_visible(rect) {
367 paint_texture_load_result(
368 ui,
369 &tlr,
370 rect,
371 self.show_loading_spinner,
372 &self.image_options,
373 );
374 }
375 texture_load_result_response(&self.source(ui.ctx()), &tlr, response)
376 }
377}
378
379#[derive(Debug, Clone, Copy)]
382pub struct ImageSize {
383 pub maintain_aspect_ratio: bool,
389
390 pub max_size: Vec2,
394
395 pub fit: ImageFit,
401}
402
403#[derive(Debug, Clone, Copy)]
407#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
408pub enum ImageFit {
409 Original { scale: f32 },
413
414 Fraction(Vec2),
416
417 Exact(Vec2),
421}
422
423impl ImageFit {
424 pub fn resolve(self, available_size: Vec2, image_size: Vec2) -> Vec2 {
425 match self {
426 Self::Original { scale } => image_size * scale,
427 Self::Fraction(fract) => available_size * fract,
428 Self::Exact(size) => size,
429 }
430 }
431}
432
433impl ImageSize {
434 pub fn hint(&self, available_size: Vec2, pixels_per_point: f32) -> SizeHint {
436 let size = match self.fit {
437 ImageFit::Original { scale } => return SizeHint::Scale(scale.ord()),
438 ImageFit::Fraction(fract) => available_size * fract,
439 ImageFit::Exact(size) => size,
440 };
441 let size = size.min(self.max_size);
442 let size = size * pixels_per_point;
443
444 match (size.x.is_finite(), size.y.is_finite()) {
446 (true, true) => SizeHint::Size(size.x.round() as u32, size.y.round() as u32),
447 (true, false) => SizeHint::Width(size.x.round() as u32),
448 (false, true) => SizeHint::Height(size.y.round() as u32),
449 (false, false) => SizeHint::Scale(pixels_per_point.ord()),
450 }
451 }
452
453 pub fn calc_size(&self, available_size: Vec2, original_image_size: Vec2) -> Vec2 {
455 let Self {
456 maintain_aspect_ratio,
457 max_size,
458 fit,
459 } = *self;
460 match fit {
461 ImageFit::Original { scale } => {
462 let image_size = original_image_size * scale;
463 if image_size.x <= max_size.x && image_size.y <= max_size.y {
464 image_size
465 } else {
466 scale_to_fit(image_size, max_size, maintain_aspect_ratio)
467 }
468 }
469 ImageFit::Fraction(fract) => {
470 let scale_to_size = (available_size * fract).min(max_size);
471 scale_to_fit(original_image_size, scale_to_size, maintain_aspect_ratio)
472 }
473 ImageFit::Exact(size) => {
474 let scale_to_size = size.min(max_size);
475 scale_to_fit(original_image_size, scale_to_size, maintain_aspect_ratio)
476 }
477 }
478 }
479}
480
481fn scale_to_fit(image_size: Vec2, available_size: Vec2, maintain_aspect_ratio: bool) -> Vec2 {
483 if maintain_aspect_ratio {
484 let ratio_x = available_size.x / image_size.x;
485 let ratio_y = available_size.y / image_size.y;
486 let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y };
487 let ratio = if ratio.is_finite() { ratio } else { 1.0 };
488 image_size * ratio
489 } else {
490 available_size
491 }
492}
493
494impl Default for ImageSize {
495 #[inline]
496 fn default() -> Self {
497 Self {
498 max_size: Vec2::INFINITY,
499 fit: ImageFit::Fraction(Vec2::new(1.0, 1.0)),
500 maintain_aspect_ratio: true,
501 }
502 }
503}
504
505#[derive(Clone)]
509pub enum ImageSource<'a> {
510 Uri(Cow<'a, str>),
519
520 Texture(SizedTexture),
525
526 Bytes {
538 uri: Cow<'static, str>,
544
545 bytes: Bytes,
546 },
547}
548
549impl<'a> std::fmt::Debug for ImageSource<'a> {
550 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
551 match self {
552 ImageSource::Bytes { uri, .. } | ImageSource::Uri(uri) => uri.as_ref().fmt(f),
553 ImageSource::Texture(st) => st.id.fmt(f),
554 }
555 }
556}
557
558impl<'a> ImageSource<'a> {
559 #[inline]
561 pub fn texture_size(&self) -> Option<Vec2> {
562 match self {
563 ImageSource::Texture(texture) => Some(texture.size),
564 ImageSource::Uri(_) | ImageSource::Bytes { .. } => None,
565 }
566 }
567
568 pub fn load(
571 self,
572 ctx: &Context,
573 texture_options: TextureOptions,
574 size_hint: SizeHint,
575 ) -> TextureLoadResult {
576 match self {
577 Self::Texture(texture) => Ok(TexturePoll::Ready { texture }),
578 Self::Uri(uri) => ctx.try_load_texture(uri.as_ref(), texture_options, size_hint),
579 Self::Bytes { uri, bytes } => {
580 ctx.include_bytes(uri.clone(), bytes);
581 ctx.try_load_texture(uri.as_ref(), texture_options, size_hint)
582 }
583 }
584 }
585
586 pub fn uri(&self) -> Option<&str> {
590 match self {
591 ImageSource::Bytes { uri, .. } | ImageSource::Uri(uri) => Some(uri),
592 ImageSource::Texture(_) => None,
593 }
594 }
595}
596
597pub fn paint_texture_load_result(
598 ui: &Ui,
599 tlr: &TextureLoadResult,
600 rect: Rect,
601 show_loading_spinner: Option<bool>,
602 options: &ImageOptions,
603) {
604 match tlr {
605 Ok(TexturePoll::Ready { texture }) => {
606 paint_texture_at(ui.painter(), rect, options, texture);
607 }
608 Ok(TexturePoll::Pending { .. }) => {
609 let show_loading_spinner =
610 show_loading_spinner.unwrap_or(ui.visuals().image_loading_spinners);
611 if show_loading_spinner {
612 Spinner::new().paint_at(ui, rect);
613 }
614 }
615 Err(_) => {
616 let font_id = TextStyle::Body.resolve(ui.style());
617 ui.painter().text(
618 rect.center(),
619 Align2::CENTER_CENTER,
620 "⚠",
621 font_id,
622 ui.visuals().error_fg_color,
623 );
624 }
625 }
626}
627
628pub fn texture_load_result_response(
630 source: &ImageSource<'_>,
631 tlr: &TextureLoadResult,
632 response: Response,
633) -> Response {
634 match tlr {
635 Ok(TexturePoll::Ready { .. }) => response,
636 Ok(TexturePoll::Pending { .. }) => {
637 let uri = source.uri().unwrap_or("image");
638 response.on_hover_text(format!("Loading {uri}…"))
639 }
640 Err(err) => {
641 let uri = source.uri().unwrap_or("image");
642 response.on_hover_text(format!("Failed loading {uri}: {err}"))
643 }
644 }
645}
646
647impl<'a> From<&'a str> for ImageSource<'a> {
648 #[inline]
649 fn from(value: &'a str) -> Self {
650 Self::Uri(value.into())
651 }
652}
653
654impl<'a> From<&'a String> for ImageSource<'a> {
655 #[inline]
656 fn from(value: &'a String) -> Self {
657 Self::Uri(value.as_str().into())
658 }
659}
660
661impl From<String> for ImageSource<'static> {
662 fn from(value: String) -> Self {
663 Self::Uri(value.into())
664 }
665}
666
667impl<'a> From<&'a Cow<'a, str>> for ImageSource<'a> {
668 #[inline]
669 fn from(value: &'a Cow<'a, str>) -> Self {
670 Self::Uri(value.clone())
671 }
672}
673
674impl<'a> From<Cow<'a, str>> for ImageSource<'a> {
675 #[inline]
676 fn from(value: Cow<'a, str>) -> Self {
677 Self::Uri(value)
678 }
679}
680
681impl<T: Into<Bytes>> From<(&'static str, T)> for ImageSource<'static> {
682 #[inline]
683 fn from((uri, bytes): (&'static str, T)) -> Self {
684 Self::Bytes {
685 uri: uri.into(),
686 bytes: bytes.into(),
687 }
688 }
689}
690
691impl<T: Into<Bytes>> From<(Cow<'static, str>, T)> for ImageSource<'static> {
692 #[inline]
693 fn from((uri, bytes): (Cow<'static, str>, T)) -> Self {
694 Self::Bytes {
695 uri,
696 bytes: bytes.into(),
697 }
698 }
699}
700
701impl<T: Into<Bytes>> From<(String, T)> for ImageSource<'static> {
702 #[inline]
703 fn from((uri, bytes): (String, T)) -> Self {
704 Self::Bytes {
705 uri: uri.into(),
706 bytes: bytes.into(),
707 }
708 }
709}
710
711impl<T: Into<SizedTexture>> From<T> for ImageSource<'static> {
712 fn from(value: T) -> Self {
713 Self::Texture(value.into())
714 }
715}
716
717#[derive(Debug, Clone)]
718#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
719pub struct ImageOptions {
720 pub uv: Rect,
722
723 pub bg_fill: Color32,
725
726 pub tint: Color32,
728
729 pub rotation: Option<(Rot2, Vec2)>,
739
740 pub rounding: Rounding,
747}
748
749impl Default for ImageOptions {
750 fn default() -> Self {
751 Self {
752 uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
753 bg_fill: Default::default(),
754 tint: Color32::WHITE,
755 rotation: None,
756 rounding: Rounding::ZERO,
757 }
758 }
759}
760
761pub fn paint_texture_at(
762 painter: &Painter,
763 rect: Rect,
764 options: &ImageOptions,
765 texture: &SizedTexture,
766) {
767 if options.bg_fill != Default::default() {
768 painter.add(RectShape::filled(rect, options.rounding, options.bg_fill));
769 }
770
771 match options.rotation {
772 Some((rot, origin)) => {
773 debug_assert!(
776 options.rounding == Rounding::ZERO,
777 "Image had both rounding and rotation. Please pick only one"
778 );
779
780 let mut mesh = Mesh::with_texture(texture.id);
781 mesh.add_rect_with_uv(rect, options.uv, options.tint);
782 mesh.rotate(rot, rect.min + origin * rect.size());
783 painter.add(Shape::mesh(mesh));
784 }
785 None => {
786 painter.add(RectShape {
787 rect,
788 rounding: options.rounding,
789 fill: options.tint,
790 stroke: Stroke::NONE,
791 blur_width: 0.0,
792 fill_texture_id: texture.id,
793 uv: options.uv,
794 });
795 }
796 }
797}
798
799fn encode_gif_uri(uri: &str, frame_index: usize) -> String {
801 format!("{uri}#{frame_index}")
802}
803
804pub fn decode_gif_uri(uri: &str) -> Result<(&str, usize), String> {
808 let (uri, index) = uri
809 .rsplit_once('#')
810 .ok_or("Failed to find index separator '#'")?;
811 let index: usize = index
812 .parse()
813 .map_err(|_err| format!("Failed to parse gif frame index: {index:?} is not an integer"))?;
814 Ok((uri, index))
815}
816
817fn is_gif_uri(uri: &str) -> bool {
819 uri.ends_with(".gif") || uri.contains(".gif#")
820}
821
822pub fn has_gif_magic_header(bytes: &[u8]) -> bool {
824 bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a")
825}
826
827fn gif_frame_index(ctx: &Context, uri: &str) -> usize {
829 let now = ctx.input(|i| Duration::from_secs_f64(i.time));
830
831 let durations: Option<GifFrameDurations> = ctx.data(|data| data.get_temp(Id::new(uri)));
832 if let Some(durations) = durations {
833 let frames: Duration = durations.0.iter().sum();
834 let pos_ms = now.as_millis() % frames.as_millis().max(1);
835 let mut cumulative_ms = 0;
836 for (i, duration) in durations.0.iter().enumerate() {
837 cumulative_ms += duration.as_millis();
838 if pos_ms < cumulative_ms {
839 let ms_until_next_frame = cumulative_ms - pos_ms;
840 ctx.request_repaint_after(Duration::from_millis(ms_until_next_frame as u64));
841 return i;
842 }
843 }
844 0
845 } else {
846 0
847 }
848}
849
850#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
851pub struct GifFrameDurations(pub Arc<Vec<Duration>>);