From 67a3fcae383044def7450b311ddc1f79e36eaae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= <1665677+jprochazk@users.noreply.github.com> Date: Wed, 13 Sep 2023 16:27:08 +0200 Subject: [PATCH] Final polish for new image loading (#3328) * add egui logo to widget gallery * improve "no image loaders" error message * rework static URIs to accept `Cow<'static>` * remove `RetainedImage` from `http_app` in `egui_demo_app` * hide `RetainedImage` from docs * use `ui.image`/`Image` over `RawImage` * remove last remanant of `RawImage` * remove unused doc link * add style option to disable image spinners * use `Into` instead of `Into` to allow configuring the underlying image * propagate `image_options` through `ImageButton` * calculate image size properly in `Button` * properly calculate size in `ImageButton` * Update crates/egui/src/widgets/image.rs Co-authored-by: Emil Ernerfeldt * improve no image loaders error message * add `size()` helper to `TexturePoll` * try get size from poll in `Button` * add `paint_at` to `Spinner` * use `Spinner::paint_at` and hover on image button response * `show_spinner` -> `show_loading_spinner` * avoid `allocate_ui` in `Image` when painting spinner * make icon smaller + remove old texture * add `load_and_calculate_size` + expose `paint_image_at` * update `egui_plot` to paint image in the right place * Add helpers for painting an ImageSource directly * Use max_size=INF as default * Use new API in WidgetGallery * Make egui_demo_app work by default * Remove Option from scale * Refactor ImageSize * Fix docstring * Small refactor --------- Co-authored-by: Emil Ernerfeldt --- crates/egui-wgpu/src/renderer.rs | 5 +- crates/egui/src/context.rs | 21 +- crates/egui/src/lib.rs | 7 +- crates/egui/src/load.rs | 126 ++--- crates/egui/src/load/bytes_loader.rs | 57 +++ crates/egui/src/load/texture_loader.rs | 60 +++ crates/egui/src/style.rs | 8 + crates/egui/src/ui.rs | 46 +- crates/egui/src/widgets/button.rs | 73 +-- crates/egui/src/widgets/image.rs | 476 +++++++++--------- crates/egui/src/widgets/mod.rs | 2 +- crates/egui/src/widgets/spinner.rs | 27 +- crates/egui_demo_app/Cargo.toml | 4 +- crates/egui_demo_app/src/apps/http_app.rs | 44 +- crates/egui_demo_app/src/apps/image_viewer.rs | 29 +- crates/egui_demo_app/src/wrap_app.rs | 1 - crates/egui_demo_lib/assets/icon.png | Bin 0 -> 2642 bytes crates/egui_demo_lib/src/color_test.rs | 4 +- .../egui_demo_lib/src/demo/widget_gallery.rs | 19 +- crates/egui_extras/src/image.rs | 2 +- crates/egui_extras/src/lib.rs | 2 + crates/egui_glium/examples/native_texture.rs | 3 +- crates/egui_plot/src/items/mod.rs | 19 +- examples/images/src/main.rs | 2 +- examples/screenshot/src/main.rs | 2 +- 25 files changed, 536 insertions(+), 503 deletions(-) create mode 100644 crates/egui/src/load/bytes_loader.rs create mode 100644 crates/egui/src/load/texture_loader.rs create mode 100644 crates/egui_demo_lib/assets/icon.png diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index c97fba88..532c7c01 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -605,9 +605,8 @@ impl Renderer { /// Get the WGPU texture and bind group associated to a texture that has been allocated by egui. /// - /// This could be used by custom paint hooks to render images that have been added through with - /// [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html) - /// or [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture). + /// This could be used by custom paint hooks to render images that have been added through + /// [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture). pub fn texture( &self, id: &epaint::TextureId, diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index c3211315..aca9d2ba 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1,5 +1,6 @@ #![warn(missing_docs)] // Let's keep `Context` well-documented. +use std::borrow::Cow; use std::sync::Arc; use crate::load::Bytes; @@ -1145,7 +1146,7 @@ impl Context { /// }); /// /// // Show the image: - /// ui.raw_image((texture.id(), texture.size_vec2())); + /// ui.image((texture.id(), texture.size_vec2())); /// } /// } /// ``` @@ -1691,14 +1692,14 @@ impl Context { let mut size = vec2(w as f32, h as f32); size *= (max_preview_size.x / size.x).min(1.0); size *= (max_preview_size.y / size.y).min(1.0); - ui.raw_image(SizedTexture::new(texture_id, size)) + ui.image(SizedTexture::new(texture_id, size)) .on_hover_ui(|ui| { // show larger on hover let max_size = 0.5 * ui.ctx().screen_rect().size(); let mut size = vec2(w as f32, h as f32); size *= max_size.x / size.x.max(max_size.x); size *= max_size.y / size.y.max(max_size.y); - ui.raw_image(SizedTexture::new(texture_id, size)); + ui.image(SizedTexture::new(texture_id, size)); }); ui.label(format!("{w} x {h}")); @@ -1911,8 +1912,8 @@ impl Context { /// Associate some static bytes with a `uri`. /// /// The same `uri` may be passed to [`Ui::image`] later to load the bytes as an image. - pub fn include_bytes(&self, uri: &'static str, bytes: impl Into) { - self.loaders().include.insert(uri, bytes.into()); + pub fn include_bytes(&self, uri: impl Into>, bytes: impl Into) { + self.loaders().include.insert(uri, bytes); } /// Returns `true` if the chain of bytes, image, or texture loaders @@ -2038,17 +2039,25 @@ impl Context { /// /// # Errors /// This may fail with: + /// - [`LoadError::NoImageLoaders`][no_image_loaders] if tbere are no registered image loaders. /// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`. /// - [`LoadError::Custom`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed. /// /// ⚠ May deadlock if called from within an `ImageLoader`! /// + /// [no_image_loaders]: crate::load::LoadError::NoImageLoaders /// [not_supported]: crate::load::LoadError::NotSupported /// [custom]: crate::load::LoadError::Custom pub fn try_load_image(&self, uri: &str, size_hint: load::SizeHint) -> load::ImageLoadResult { crate::profile_function!(); - for loader in self.loaders().image.lock().iter() { + let loaders = self.loaders(); + let loaders = loaders.image.lock(); + if loaders.is_empty() { + return Err(load::LoadError::NoImageLoaders); + } + + for loader in loaders.iter() { match loader.load(self, uri, size_hint) { Err(load::LoadError::NotSupported) => continue, result => return result, diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 0d669e70..4bc2dbbf 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -84,7 +84,7 @@ //! ui.separator(); //! //! # let my_image = egui::TextureId::default(); -//! ui.raw_image((my_image, egui::Vec2::new(640.0, 480.0))); +//! ui.image((my_image, egui::Vec2::new(640.0, 480.0))); //! //! ui.collapsing("Click to see what is hidden!", |ui| { //! ui.label("Not much, as it turns out"); @@ -442,7 +442,10 @@ pub fn warn_if_debug_build(ui: &mut crate::Ui) { #[macro_export] macro_rules! include_image { ($path: literal) => { - $crate::ImageSource::Bytes($path, $crate::load::Bytes::Static(include_bytes!($path))) + $crate::ImageSource::Bytes( + ::std::borrow::Cow::Borrowed($path), + $crate::load::Bytes::Static(include_bytes!($path)), + ) }; } diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs index a325fc00..2bd6c632 100644 --- a/crates/egui/src/load.rs +++ b/crates/egui/src/load.rs @@ -52,6 +52,11 @@ //! For example, a loader may determine that it doesn't support loading a specific URI //! if the protocol does not match what it expects. +mod bytes_loader; +mod texture_loader; + +use self::bytes_loader::DefaultBytesLoader; +use self::texture_loader::DefaultTextureLoader; use crate::Context; use ahash::HashMap; use epaint::mutex::Mutex; @@ -59,6 +64,7 @@ use epaint::util::FloatOrd; use epaint::util::OrderedFloat; use epaint::TextureHandle; use epaint::{textures::TextureOptions, ColorImage, TextureId, Vec2}; +use std::borrow::Cow; use std::fmt::Debug; use std::ops::Deref; use std::{error::Error as StdError, fmt::Display, sync::Arc}; @@ -66,6 +72,9 @@ use std::{error::Error as StdError, fmt::Display, sync::Arc}; /// Represents a failed attempt at loading an image. #[derive(Clone, Debug)] pub enum LoadError { + /// There are no image loaders installed. + NoImageLoaders, + /// This loader does not support this protocol or image format. NotSupported, @@ -76,6 +85,9 @@ pub enum LoadError { impl Display for LoadError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + LoadError::NoImageLoaders => f.write_str( + "No image loaders are installed. If you're trying to load some images \ + for the first time, follow the steps outlined in https://docs.rs/egui/latest/egui/load/index.html"), LoadError::NotSupported => f.write_str("not supported"), LoadError::Custom(message) => f.write_str(message), } @@ -342,7 +354,7 @@ pub trait ImageLoader { } /// A texture with a known size. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct SizedTexture { pub id: TextureId, pub size: Vec2, @@ -370,7 +382,13 @@ impl SizedTexture { impl From<(TextureId, Vec2)> for SizedTexture { #[inline] fn from((id, size): (TextureId, Vec2)) -> Self { - SizedTexture { id, size } + Self { id, size } + } +} + +impl<'a> From<&'a TextureHandle> for SizedTexture { + fn from(handle: &'a TextureHandle) -> Self { + Self::from_handle(handle) } } @@ -379,7 +397,7 @@ impl From<(TextureId, Vec2)> for SizedTexture { /// This is similar to [`std::task::Poll`], but the `Pending` variant /// contains an optional `size`, which may be used during layout to /// pre-allocate space the image. -#[derive(Clone)] +#[derive(Clone, Copy)] pub enum TexturePoll { /// Texture is loading. Pending { @@ -391,6 +409,15 @@ pub enum TexturePoll { Ready { texture: SizedTexture }, } +impl TexturePoll { + pub fn size(self) -> Option { + match self { + TexturePoll::Pending { size } => size, + TexturePoll::Ready { texture } => Some(texture.size), + } + } +} + pub type TextureLoadResult = Result; /// Represents a loader capable of loading a full texture. @@ -447,99 +474,6 @@ pub trait TextureLoader { fn byte_size(&self) -> usize; } -#[derive(Default)] -pub(crate) struct DefaultBytesLoader { - cache: Mutex>, -} - -impl DefaultBytesLoader { - pub(crate) fn insert(&self, uri: &'static str, bytes: impl Into) { - self.cache.lock().entry(uri).or_insert_with(|| bytes.into()); - } -} - -impl BytesLoader for DefaultBytesLoader { - fn id(&self) -> &str { - generate_loader_id!(DefaultBytesLoader) - } - - fn load(&self, _: &Context, uri: &str) -> BytesLoadResult { - match self.cache.lock().get(uri).cloned() { - Some(bytes) => Ok(BytesPoll::Ready { - size: None, - bytes, - mime: None, - }), - None => Err(LoadError::NotSupported), - } - } - - fn forget(&self, uri: &str) { - let _ = self.cache.lock().remove(uri); - } - - fn forget_all(&self) { - self.cache.lock().clear(); - } - - fn byte_size(&self) -> usize { - self.cache.lock().values().map(|bytes| bytes.len()).sum() - } -} - -#[derive(Default)] -struct DefaultTextureLoader { - cache: Mutex>, -} - -impl TextureLoader for DefaultTextureLoader { - fn id(&self) -> &str { - generate_loader_id!(DefaultTextureLoader) - } - - fn load( - &self, - ctx: &Context, - uri: &str, - texture_options: TextureOptions, - size_hint: SizeHint, - ) -> TextureLoadResult { - let mut cache = self.cache.lock(); - if let Some(handle) = cache.get(&(uri.into(), texture_options)) { - let texture = SizedTexture::from_handle(handle); - Ok(TexturePoll::Ready { texture }) - } else { - match ctx.try_load_image(uri, size_hint)? { - ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }), - ImagePoll::Ready { image } => { - let handle = ctx.load_texture(uri, image, texture_options); - let texture = SizedTexture::from_handle(&handle); - cache.insert((uri.into(), texture_options), handle); - Ok(TexturePoll::Ready { texture }) - } - } - } - } - - fn forget(&self, uri: &str) { - self.cache.lock().retain(|(u, _), _| u != uri); - } - - fn forget_all(&self) { - self.cache.lock().clear(); - } - - fn end_frame(&self, _: usize) {} - - fn byte_size(&self) -> usize { - self.cache - .lock() - .values() - .map(|texture| texture.byte_size()) - .sum() - } -} - type BytesLoaderImpl = Arc; type ImageLoaderImpl = Arc; type TextureLoaderImpl = Arc; diff --git a/crates/egui/src/load/bytes_loader.rs b/crates/egui/src/load/bytes_loader.rs new file mode 100644 index 00000000..451ce150 --- /dev/null +++ b/crates/egui/src/load/bytes_loader.rs @@ -0,0 +1,57 @@ +use super::*; + +#[derive(Default)] +pub struct DefaultBytesLoader { + cache: Mutex, Bytes>>, +} + +impl DefaultBytesLoader { + pub fn insert(&self, uri: impl Into>, bytes: impl Into) { + self.cache + .lock() + .entry(uri.into()) + .or_insert_with_key(|uri| { + let bytes: Bytes = bytes.into(); + + #[cfg(feature = "log")] + log::trace!("loaded {} bytes for uri {uri:?}", bytes.len()); + + bytes + }); + } +} + +impl BytesLoader for DefaultBytesLoader { + fn id(&self) -> &str { + generate_loader_id!(DefaultBytesLoader) + } + + fn load(&self, _: &Context, uri: &str) -> BytesLoadResult { + match self.cache.lock().get(uri).cloned() { + Some(bytes) => Ok(BytesPoll::Ready { + size: None, + bytes, + mime: None, + }), + None => Err(LoadError::NotSupported), + } + } + + fn forget(&self, uri: &str) { + #[cfg(feature = "log")] + log::trace!("forget {uri:?}"); + + let _ = self.cache.lock().remove(uri); + } + + fn forget_all(&self) { + #[cfg(feature = "log")] + log::trace!("forget all"); + + self.cache.lock().clear(); + } + + fn byte_size(&self) -> usize { + self.cache.lock().values().map(|bytes| bytes.len()).sum() + } +} diff --git a/crates/egui/src/load/texture_loader.rs b/crates/egui/src/load/texture_loader.rs new file mode 100644 index 00000000..89d616e4 --- /dev/null +++ b/crates/egui/src/load/texture_loader.rs @@ -0,0 +1,60 @@ +use super::*; + +#[derive(Default)] +pub struct DefaultTextureLoader { + cache: Mutex>, +} + +impl TextureLoader for DefaultTextureLoader { + fn id(&self) -> &str { + crate::generate_loader_id!(DefaultTextureLoader) + } + + fn load( + &self, + ctx: &Context, + uri: &str, + texture_options: TextureOptions, + size_hint: SizeHint, + ) -> TextureLoadResult { + let mut cache = self.cache.lock(); + if let Some(handle) = cache.get(&(uri.into(), texture_options)) { + let texture = SizedTexture::from_handle(handle); + Ok(TexturePoll::Ready { texture }) + } else { + match ctx.try_load_image(uri, size_hint)? { + ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }), + ImagePoll::Ready { image } => { + let handle = ctx.load_texture(uri, image, texture_options); + let texture = SizedTexture::from_handle(&handle); + cache.insert((uri.into(), texture_options), handle); + Ok(TexturePoll::Ready { texture }) + } + } + } + } + + fn forget(&self, uri: &str) { + #[cfg(feature = "log")] + log::trace!("forget {uri:?}"); + + self.cache.lock().retain(|(u, _), _| u != uri); + } + + fn forget_all(&self) { + #[cfg(feature = "log")] + log::trace!("forget all"); + + self.cache.lock().clear(); + } + + fn end_frame(&self, _: usize) {} + + fn byte_size(&self) -> usize { + self.cache + .lock() + .values() + .map(|texture| texture.byte_size()) + .sum() + } +} diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index e7a2c76e..69185dbe 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -207,6 +207,9 @@ pub struct Style { /// /// This only affects a few egui widgets. pub explanation_tooltips: bool, + + /// Show a spinner when loading an image. + pub image_loading_spinners: bool, } impl Style { @@ -738,6 +741,7 @@ impl Default for Style { animation_time: 1.0 / 12.0, debug: Default::default(), explanation_tooltips: false, + image_loading_spinners: true, } } } @@ -990,6 +994,7 @@ impl Style { animation_time, debug, explanation_tooltips, + image_loading_spinners, } = self; visuals.light_dark_radio_buttons(ui); @@ -1057,6 +1062,9 @@ impl Style { "Show explanatory text when hovering DragValue:s and other egui widgets", ); + ui.checkbox(image_loading_spinners, "Image loading spinners") + .on_hover_text("Show a spinner when an Image is loading"); + ui.vertical_centered(|ui| reset_button(ui, self)); } } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 5b342b20..4886e2d7 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -5,7 +5,6 @@ use std::sync::Arc; use epaint::mutex::RwLock; -use crate::load::SizedTexture; use crate::{ containers::*, ecolor::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer, util::IdTypeMap, widgets::*, *, @@ -1582,47 +1581,10 @@ impl Ui { /// from a file with a statically known path, unless you really want to /// load it at runtime instead! /// - /// See also [`crate::Image`], [`crate::ImageSource`] and [`Self::raw_image`]. + /// See also [`crate::Image`], [`crate::ImageSource`]. #[inline] pub fn image<'a>(&mut self, source: impl Into>) -> Response { - Image::new(source.into()).ui(self) - } - - /// Show an image created from a sized texture. - /// - /// You may use this method over [`Ui::image`] if you already have a [`TextureHandle`] - /// or a [`SizedTexture`]. - /// - /// ``` - /// # egui::__run_test_ui(|ui| { - /// struct MyImage { - /// texture: Option, - /// } - /// - /// impl MyImage { - /// fn ui(&mut self, ui: &mut egui::Ui) { - /// let texture = self - /// .texture - /// .get_or_insert_with(|| { - /// // Load the texture only once. - /// ui.ctx().load_texture( - /// "my-image", - /// egui::ColorImage::example(), - /// Default::default() - /// ) - /// }); - /// - /// // Show the image: - /// ui.raw_image((texture.id(), texture.size_vec2())); - /// } - /// } - /// # }); - /// ``` - /// - /// See also [`crate::RawImage`]. - #[inline] - pub fn raw_image(&mut self, texture: impl Into) -> Response { - RawImage::new(texture).ui(self) + Image::new(source).ui(self) } } @@ -2236,13 +2198,13 @@ impl Ui { #[inline] pub fn menu_image_button<'a, R>( &mut self, - image_source: impl Into>, + image: impl Into>, add_contents: impl FnOnce(&mut Ui) -> R, ) -> InnerResponse> { if let Some(menu_state) = self.menu_state.clone() { menu::submenu_button(self, menu_state, String::new(), add_contents) } else { - menu::menu_image_button(self, ImageButton::new(image_source), add_contents) + menu::menu_image_button(self, ImageButton::new(image), add_contents) } } } diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 38353b14..6c834fd8 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -21,7 +21,7 @@ use crate::*; /// # }); /// ``` #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] -pub struct Button { +pub struct Button<'a> { text: WidgetText, shortcut_text: WidgetText, wrap: Option, @@ -34,10 +34,10 @@ pub struct Button { frame: Option, min_size: Vec2, rounding: Option, - image: Option, + image: Option>, } -impl Button { +impl<'a> Button<'a> { pub fn new(text: impl Into) -> Self { Self { text: text.into(), @@ -56,16 +56,9 @@ impl Button { /// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the provided size. #[allow(clippy::needless_pass_by_value)] - pub fn image_and_text( - texture_id: TextureId, - image_size: impl Into, - text: impl Into, - ) -> Self { + pub fn image_and_text(image: impl Into>, text: impl Into) -> Self { Self { - image: Some(widgets::RawImage::new(SizedTexture { - id: texture_id, - size: image_size.into(), - })), + image: Some(image.into()), ..Self::new(text) } } @@ -142,7 +135,7 @@ impl Button { } } -impl Widget for Button { +impl Widget for Button<'_> { fn ui(self, ui: &mut Ui) -> Response { let Button { text, @@ -158,6 +151,11 @@ impl Widget for Button { image, } = self; + let image_size = image + .as_ref() + .and_then(|image| image.load_and_calculate_size(ui, ui.available_size())) + .unwrap_or(Vec2::ZERO); + let frame = frame.unwrap_or_else(|| ui.visuals().button_frame); let mut button_padding = ui.spacing().button_padding; @@ -166,8 +164,8 @@ impl Widget for Button { } let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x; - if let Some(image) = &image { - text_wrap_width -= image.size().x + ui.spacing().icon_spacing; + if image.is_some() { + text_wrap_width -= image_size.x + ui.spacing().icon_spacing; } if !shortcut_text.is_empty() { text_wrap_width -= 60.0; // Some space for the shortcut text (which we never wrap). @@ -178,9 +176,9 @@ impl Widget for Button { .then(|| shortcut_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button)); let mut desired_size = text.size(); - if let Some(image) = &image { - desired_size.x += image.size().x + ui.spacing().icon_spacing; - desired_size.y = desired_size.y.max(image.size().y); + if image.is_some() { + desired_size.x += image_size.x + ui.spacing().icon_spacing; + desired_size.y = desired_size.y.max(image_size.y); } if let Some(shortcut_text) = &shortcut_text { desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x; @@ -192,7 +190,7 @@ impl Widget for Button { } desired_size = desired_size.at_least(min_size); - let (rect, response) = ui.allocate_at_least(desired_size, sense); + let (rect, mut response) = ui.allocate_at_least(desired_size, sense); response.widget_info(|| WidgetInfo::labeled(WidgetType::Button, text.text())); if ui.is_rect_visible(rect) { @@ -206,10 +204,10 @@ impl Widget for Button { .rect(rect.expand(visuals.expansion), rounding, fill, stroke); } - let text_pos = if let Some(image) = &image { + let text_pos = if image.is_some() { let icon_spacing = ui.spacing().icon_spacing; pos2( - rect.min.x + button_padding.x + image.size().x + icon_spacing, + rect.min.x + button_padding.x + image_size.x + icon_spacing, rect.center().y - 0.5 * text.size().y, ) } else { @@ -235,11 +233,23 @@ impl Widget for Button { let image_rect = Rect::from_min_size( pos2( rect.min.x + button_padding.x, - rect.center().y - 0.5 - (image.size().y / 2.0), + rect.center().y - 0.5 - (image_size.y / 2.0), ), - image.size(), + image_size, ); - image.paint_at(ui, image_rect); + let tlr = image.load(ui); + let show_loading_spinner = image + .show_loading_spinner + .unwrap_or(ui.style().image_loading_spinners); + widgets::image::paint_texture_load_result( + ui, + &tlr, + image_rect, + show_loading_spinner, + image.image_options(), + ); + response = + widgets::image::texture_load_result_response(image.source(), &tlr, response); } } @@ -481,9 +491,9 @@ pub struct ImageButton<'a> { } impl<'a> ImageButton<'a> { - pub fn new(source: impl Into>) -> Self { + pub fn new(image: impl Into>) -> Self { Self { - image: Image::new(source.into()), + image: image.into(), sense: Sense::click(), frame: true, selected: false, @@ -564,9 +574,9 @@ impl<'a> ImageButton<'a> { .align_size_within_rect(texture.size, rect.shrink2(padding)); // let image_rect = image_rect.expand2(expansion); // can make it blurry, so let's not let image_options = ImageOptions { - rounding, - ..Default::default() - }; // apply rounding to the image + rounding, // apply rounding to the image + ..self.image.image_options().clone() + }; crate::widgets::image::paint_image_at(ui, image_rect, &image_options, texture); // Draw frame outline: @@ -581,7 +591,10 @@ impl<'a> ImageButton<'a> { impl<'a> Widget for ImageButton<'a> { fn ui(self, ui: &mut Ui) -> Response { match self.image.load(ui) { - Ok(TexturePoll::Ready { texture }) => self.show(ui, &texture), + Ok(TexturePoll::Ready { mut texture }) => { + texture.size = self.image.calculate_size(ui.available_size(), texture.size); + self.show(ui, &texture) + } Ok(TexturePoll::Pending { .. }) => ui .spinner() .on_hover_text(format!("Loading {:?}…", self.image.uri())), diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index cc98559d..163e264e 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -17,8 +17,6 @@ use epaint::{util::FloatOrd, RectShape}; /// - [`ImageSource::Bytes`] will also load the image using the [asynchronous loading process][`load`], but with lower latency. /// - [`ImageSource::Texture`] will use the provided texture. /// -/// To use a texture you already have with a simpler API, consider using [`RawImage`]. -/// /// See [`load`] for more information. #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] #[derive(Debug, Clone)] @@ -28,18 +26,36 @@ pub struct Image<'a> { image_options: ImageOptions, sense: Sense, size: ImageSize, + pub(crate) show_loading_spinner: Option, } impl<'a> Image<'a> { /// Load the image from some source. - pub fn new(source: ImageSource<'a>) -> Self { - Self { - source, - texture_options: Default::default(), - image_options: Default::default(), - sense: Sense::hover(), - size: Default::default(), + pub fn new(source: impl Into>) -> Self { + fn new_mono(source: ImageSource<'_>) -> Image<'_> { + let size = if let ImageSource::Texture(tex) = &source { + // User is probably expecting their texture to have + // the exact size of the provided `SizedTexture`. + ImageSize { + maintain_aspect_ratio: true, + max_size: Vec2::INFINITY, + fit: ImageFit::Exact(tex.size), + } + } else { + Default::default() + }; + + Image { + source, + texture_options: Default::default(), + image_options: Default::default(), + sense: Sense::hover(), + size, + show_loading_spinner: None, + } } + + new_mono(source.into()) } /// Load the image from a URI. @@ -52,15 +68,15 @@ impl<'a> Image<'a> { /// Load the image from an existing texture. /// /// See [`ImageSource::Texture`]. - pub fn from_texture(texture: SizedTexture) -> Self { - Self::new(ImageSource::Texture(texture)) + pub fn from_texture(texture: impl Into) -> Self { + Self::new(ImageSource::Texture(texture.into())) } /// Load the image from some raw bytes. /// /// See [`ImageSource::Bytes`]. - pub fn from_bytes(uri: &'static str, bytes: impl Into) -> Self { - Self::new(ImageSource::Bytes(uri, bytes.into())) + pub fn from_bytes(uri: impl Into>, bytes: impl Into) -> Self { + Self::new(ImageSource::Bytes(uri.into(), bytes.into())) } /// Texture options used when creating the texture. @@ -75,10 +91,7 @@ impl<'a> Image<'a> { /// No matter what the image is scaled to, it will never exceed this limit. #[inline] pub fn max_width(mut self, width: f32) -> Self { - match self.size.max_size.as_mut() { - Some(max_size) => max_size.x = width, - None => self.size.max_size = Some(Vec2::new(width, f32::INFINITY)), - } + self.size.max_size.x = width; self } @@ -87,10 +100,7 @@ impl<'a> Image<'a> { /// No matter what the image is scaled to, it will never exceed this limit. #[inline] pub fn max_height(mut self, height: f32) -> Self { - match self.size.max_size.as_mut() { - Some(max_size) => max_size.y = height, - None => self.size.max_size = Some(Vec2::new(f32::INFINITY, height)), - } + self.size.max_size.y = height; self } @@ -98,7 +108,7 @@ impl<'a> Image<'a> { /// /// No matter what the image is scaled to, it will never exceed this limit. #[inline] - pub fn max_size(mut self, size: Option) -> Self { + pub fn max_size(mut self, size: Vec2) -> Self { self.size.max_size = size; self } @@ -110,14 +120,14 @@ impl<'a> Image<'a> { self } - /// Fit the image to its original size. + /// Fit the image to its original size with some scaling. /// /// This will cause the image to overflow if it is larger than the available space. /// /// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit. #[inline] - pub fn fit_to_original_size(mut self, scale: Option) -> Self { - self.size.fit = ImageFit::Original(scale); + pub fn fit_to_original_size(mut self, scale: f32) -> Self { + self.size.fit = ImageFit::Original { scale }; self } @@ -157,18 +167,21 @@ impl<'a> Image<'a> { } /// Select UV range. Default is (0,0) in top-left, (1,1) bottom right. + #[inline] pub fn uv(mut self, uv: impl Into) -> Self { self.image_options.uv = uv.into(); self } /// A solid color to put behind the image. Useful for transparent images. + #[inline] pub fn bg_fill(mut self, bg_fill: impl Into) -> Self { self.image_options.bg_fill = bg_fill.into(); self } /// Multiply image color with this. Default is WHITE (no tint). + #[inline] pub fn tint(mut self, tint: impl Into) -> Self { self.image_options.tint = tint.into(); self @@ -183,6 +196,7 @@ impl<'a> Image<'a> { /// /// Due to limitations in the current implementation, /// this will turn off rounding of the image. + #[inline] pub fn rotate(mut self, angle: f32, origin: Vec2) -> Self { self.image_options.rotation = Some((Rot2::from_angle(angle), origin)); self.image_options.rounding = Rounding::ZERO; // incompatible with rotation @@ -195,6 +209,7 @@ impl<'a> Image<'a> { /// /// Due to limitations in the current implementation, /// this will turn off any rotation of the image. + #[inline] pub fn rounding(mut self, rounding: impl Into) -> Self { self.image_options.rounding = rounding.into(); if self.image_options.rounding != Rounding::ZERO { @@ -202,14 +217,36 @@ impl<'a> Image<'a> { } self } + + /// Show a spinner when the image is loading. + /// + /// By default this uses the value of [`Style::image_loading_spinners`]. + #[inline] + pub fn show_loading_spinner(mut self, show: bool) -> Self { + self.show_loading_spinner = Some(show); + self + } +} + +impl<'a, T: Into>> From for Image<'a> { + fn from(value: T) -> Self { + Image::new(value) + } } impl<'a> Image<'a> { /// Returns the size the image will occupy in the final UI. + #[inline] pub fn calculate_size(&self, available_size: Vec2, image_size: Vec2) -> Vec2 { self.size.get(available_size, image_size) } + pub fn load_and_calculate_size(&self, ui: &mut Ui, available_size: Vec2) -> Option { + let image_size = self.load(ui).ok()?.size()?; + Some(self.size.get(available_size, image_size)) + } + + #[inline] pub fn size(&self) -> Option { match &self.source { ImageSource::Texture(texture) => Some(texture.size), @@ -217,6 +254,12 @@ impl<'a> Image<'a> { } } + #[inline] + pub fn image_options(&self) -> &ImageOptions { + &self.image_options + } + + #[inline] pub fn source(&self) -> &ImageSource<'a> { &self.source } @@ -224,13 +267,9 @@ impl<'a> Image<'a> { /// Get the `uri` that this image was constructed from. /// /// This will return `` for [`ImageSource::Texture`]. + #[inline] pub fn uri(&self) -> &str { - match &self.source { - ImageSource::Bytes(uri, _) => uri, - ImageSource::Uri(uri) => uri, - // Note: texture source is never in "loading" state - ImageSource::Texture(_) => "", - } + self.source.uri().unwrap_or("") } /// Load the image from its [`Image::source`], returning the resulting [`SizedTexture`]. @@ -239,24 +278,13 @@ impl<'a> Image<'a> { /// /// May fail if they underlying [`Context::try_load_texture`] call fails. pub fn load(&self, ui: &Ui) -> TextureLoadResult { - match self.source.clone() { - ImageSource::Texture(texture) => Ok(TexturePoll::Ready { texture }), - ImageSource::Uri(uri) => ui.ctx().try_load_texture( - uri.as_ref(), - self.texture_options, - self.size.hint(ui.available_size()), - ), - ImageSource::Bytes(uri, bytes) => { - ui.ctx().include_bytes(uri.as_ref(), bytes); - ui.ctx().try_load_texture( - uri.as_ref(), - self.texture_options, - self.size.hint(ui.available_size()), - ) - } - } + let size_hint = self.size.hint(ui.available_size()); + self.source + .clone() + .load(ui.ctx(), self.texture_options, size_hint) } + #[inline] pub fn paint_at(&self, ui: &mut Ui, rect: Rect, texture: &SizedTexture) { paint_image_at(ui, rect, &self.image_options, texture); } @@ -265,27 +293,28 @@ impl<'a> Image<'a> { impl<'a> Widget for Image<'a> { fn ui(self, ui: &mut Ui) -> Response { match self.load(ui) { - Ok(TexturePoll::Ready { texture }) => { - let size = self.calculate_size(ui.available_size(), texture.size); - let (rect, response) = ui.allocate_exact_size(size, self.sense); - self.paint_at(ui, rect, &texture); - response - } - Ok(TexturePoll::Pending { size }) => match size { - Some(size) => { - let size = self.calculate_size(ui.available_size(), size); - ui.allocate_ui(size, |ui| { - ui.with_layout(Layout::centered_and_justified(Direction::TopDown), |ui| { - ui.spinner() - .on_hover_text(format!("Loading {:?}…", self.uri())) - }) - }) - .response + Ok(texture_poll) => { + let texture_size = texture_poll.size(); + let texture_size = + texture_size.unwrap_or_else(|| Vec2::splat(ui.style().spacing.interact_size.y)); + let ui_size = self.calculate_size(ui.available_size(), texture_size); + let (rect, response) = ui.allocate_exact_size(ui_size, self.sense); + match texture_poll { + TexturePoll::Ready { texture } => { + self.paint_at(ui, rect, &texture); + response + } + TexturePoll::Pending { .. } => { + let show_spinner = self + .show_loading_spinner + .unwrap_or(ui.style().image_loading_spinners); + if show_spinner { + Spinner::new().paint_at(ui, response.rect); + } + response.on_hover_text(format!("Loading {:?}…", self.uri())) + } } - None => ui - .spinner() - .on_hover_text(format!("Loading {:?}…", self.uri())), - }, + } Err(err) => ui .colored_label(ui.visuals().error_fg_color, "⚠") .on_hover_text(err.to_string()), @@ -306,8 +335,8 @@ pub struct ImageSize { /// Determines the maximum size of the image. /// - /// Defaults to `None` - pub max_size: Option, + /// Defaults to `Vec2::INFINITY` (no limit). + pub max_size: Vec2, /// Determines how the image should shrink/expand/stretch/etc. to fit within its allocated space. /// @@ -323,8 +352,8 @@ pub struct ImageSize { #[derive(Debug, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum ImageFit { - /// Fit the image to its original size, optionally scaling it by some factor. - Original(Option), + /// Fit the image to its original size, scaled by some factor. + Original { scale: f32 }, /// Fit the image to a fraction of the available size. Fraction(Vec2), @@ -333,6 +362,16 @@ pub enum ImageFit { Exact(Vec2), } +impl ImageFit { + pub fn resolve(self, available_size: Vec2, image_size: Vec2) -> Vec2 { + match self { + ImageFit::Original { scale } => image_size * scale, + ImageFit::Fraction(fract) => available_size * fract, + ImageFit::Exact(size) => size, + } + } +} + impl ImageSize { fn hint(&self, available_size: Vec2) -> SizeHint { if self.maintain_aspect_ratio { @@ -340,15 +379,12 @@ impl ImageSize { }; let fit = match self.fit { - ImageFit::Original(scale) => return SizeHint::Scale(scale.unwrap_or(1.0).ord()), + ImageFit::Original { scale } => return SizeHint::Scale(scale.ord()), ImageFit::Fraction(fract) => available_size * fract, ImageFit::Exact(size) => size, }; - let fit = match self.max_size { - Some(extent) => fit.min(extent), - None => fit, - }; + let fit = fit.min(self.max_size); // `inf` on an axis means "any value" match (fit.x.is_finite(), fit.y.is_finite()) { @@ -360,74 +396,50 @@ impl ImageSize { } fn get(&self, available_size: Vec2, image_size: Vec2) -> Vec2 { - match self.fit { - ImageFit::Original(scale) => { - let image_size = image_size * scale.unwrap_or(1.0); - - if let Some(available_size) = self.max_size { - if image_size.x < available_size.x && image_size.y < available_size.y { - return image_size; - } - - if self.maintain_aspect_ratio { - let ratio_x = available_size.x / image_size.x; - let ratio_y = available_size.y / image_size.y; - let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y }; - let ratio = if ratio.is_infinite() { 1.0 } else { ratio }; - - return Vec2::new(image_size.x * ratio, image_size.y * ratio); - } else { - return image_size.min(available_size); - } + let Self { + maintain_aspect_ratio, + max_size, + fit, + } = *self; + match fit { + ImageFit::Original { scale } => { + let image_size = image_size * scale; + if image_size.x <= max_size.x && image_size.y <= max_size.y { + image_size + } else { + scale_to_fit(image_size, max_size, maintain_aspect_ratio) } - - image_size } ImageFit::Fraction(fract) => { - let available_size = available_size * fract; - let available_size = match self.max_size { - Some(max_size) => available_size.min(max_size), - None => available_size, - }; - - if self.maintain_aspect_ratio { - let ratio_x = available_size.x / image_size.x; - let ratio_y = available_size.y / image_size.y; - let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y }; - let ratio = if ratio.is_infinite() { 1.0 } else { ratio }; - - return Vec2::new(image_size.x * ratio, image_size.y * ratio); - } - - available_size + let scale_to_size = (available_size * fract).min(max_size); + scale_to_fit(image_size, scale_to_size, maintain_aspect_ratio) } ImageFit::Exact(size) => { - let available_size = size; - let available_size = match self.max_size { - Some(max_size) => available_size.min(max_size), - None => available_size, - }; - - if self.maintain_aspect_ratio { - let ratio_x = available_size.x / image_size.x; - let ratio_y = available_size.y / image_size.y; - let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y }; - let ratio = if ratio.is_infinite() { 1.0 } else { ratio }; - - return Vec2::new(image_size.x * ratio, image_size.y * ratio); - } - - available_size + let scale_to_size = size.min(max_size); + scale_to_fit(image_size, scale_to_size, maintain_aspect_ratio) } } } } +// TODO: unit-tests +fn scale_to_fit(image_size: Vec2, available_size: Vec2, maintain_aspect_ratio: bool) -> Vec2 { + if maintain_aspect_ratio { + let ratio_x = available_size.x / image_size.x; + let ratio_y = available_size.y / image_size.y; + let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y }; + let ratio = if ratio.is_finite() { ratio } else { 1.0 }; + image_size * ratio + } else { + available_size + } +} + impl Default for ImageSize { #[inline] fn default() -> Self { Self { - max_size: None, + max_size: Vec2::INFINITY, fit: ImageFit::Fraction(Vec2::new(1.0, 1.0)), maintain_aspect_ratio: true, } @@ -452,8 +464,6 @@ pub enum ImageSource<'a> { /// /// The user is responsible for loading the texture, determining its size, /// and allocating a [`TextureId`] for it. - /// - /// Note that a simpler API for this exists in [`RawImage`]. Texture(SizedTexture), /// Load the image from some raw bytes. @@ -467,7 +477,84 @@ pub enum ImageSource<'a> { /// See also [`include_image`] for an easy way to load and display static images. /// /// See [`crate::load`] for more information. - Bytes(&'static str, Bytes), + Bytes(Cow<'static, str>, Bytes), +} + +impl<'a> ImageSource<'a> { + /// # Errors + /// Failure to load the texture. + pub fn load( + self, + ctx: &Context, + texture_options: TextureOptions, + size_hint: SizeHint, + ) -> TextureLoadResult { + match self { + Self::Texture(texture) => Ok(TexturePoll::Ready { texture }), + Self::Uri(uri) => ctx.try_load_texture(uri.as_ref(), texture_options, size_hint), + Self::Bytes(uri, bytes) => { + ctx.include_bytes(uri.clone(), bytes); + ctx.try_load_texture(uri.as_ref(), texture_options, size_hint) + } + } + } + + /// Get the `uri` that this image was constructed from. + /// + /// This will return `None` for [`Self::Texture`]. + pub fn uri(&self) -> Option<&str> { + match self { + ImageSource::Bytes(uri, _) | ImageSource::Uri(uri) => Some(uri), + ImageSource::Texture(_) => None, + } + } +} + +pub fn paint_texture_load_result( + ui: &Ui, + tlr: &TextureLoadResult, + rect: Rect, + show_loading_spinner: bool, + options: &ImageOptions, +) { + match tlr { + Ok(TexturePoll::Ready { texture }) => { + paint_image_at(ui, rect, options, texture); + } + Ok(TexturePoll::Pending { .. }) => { + if show_loading_spinner { + Spinner::new().paint_at(ui, rect); + } + } + Err(_) => { + let font_id = TextStyle::Body.resolve(ui.style()); + ui.painter().text( + rect.center(), + Align2::CENTER_CENTER, + "⚠", + font_id, + ui.visuals().error_fg_color, + ); + } + } +} + +pub fn texture_load_result_response( + source: &ImageSource<'_>, + tlr: &TextureLoadResult, + response: Response, +) -> Response { + match tlr { + Ok(TexturePoll::Ready { .. }) => response, + Ok(TexturePoll::Pending { .. }) => { + if let Some(uri) = source.uri() { + response.on_hover_text(format!("Loading {uri}…")) + } else { + response.on_hover_text("Loading image…") + } + } + Err(err) => response.on_hover_text(err.to_string()), + } } impl<'a> From<&'a str> for ImageSource<'a> { @@ -507,123 +594,30 @@ impl<'a> From> for ImageSource<'a> { impl> From<(&'static str, T)> for ImageSource<'static> { #[inline] fn from((uri, bytes): (&'static str, T)) -> Self { + Self::Bytes(uri.into(), bytes.into()) + } +} + +impl> From<(Cow<'static, str>, T)> for ImageSource<'static> { + #[inline] + fn from((uri, bytes): (Cow<'static, str>, T)) -> Self { Self::Bytes(uri, bytes.into()) } } +impl> From<(String, T)> for ImageSource<'static> { + #[inline] + fn from((uri, bytes): (String, T)) -> Self { + Self::Bytes(uri.into(), bytes.into()) + } +} + impl> From for ImageSource<'static> { fn from(value: T) -> Self { Self::Texture(value.into()) } } -/// A widget which displays a sized texture. -#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] -#[derive(Debug, Clone)] -pub struct RawImage { - texture: SizedTexture, - texture_options: TextureOptions, - image_options: ImageOptions, - sense: Sense, -} - -impl RawImage { - /// Load the image from some source. - pub fn new(texture: impl Into) -> Self { - Self { - texture: texture.into(), - texture_options: Default::default(), - image_options: Default::default(), - sense: Sense::hover(), - } - } - - /// Texture options used when creating the texture. - #[inline] - pub fn texture_options(mut self, texture_options: TextureOptions) -> Self { - self.texture_options = texture_options; - self - } - - /// Make the image respond to clicks and/or drags. - #[inline] - pub fn sense(mut self, sense: Sense) -> Self { - self.sense = sense; - self - } - - /// Select UV range. Default is (0,0) in top-left, (1,1) bottom right. - pub fn uv(mut self, uv: impl Into) -> Self { - self.image_options.uv = uv.into(); - self - } - - /// A solid color to put behind the image. Useful for transparent images. - pub fn bg_fill(mut self, bg_fill: impl Into) -> Self { - self.image_options.bg_fill = bg_fill.into(); - self - } - - /// Multiply image color with this. Default is WHITE (no tint). - pub fn tint(mut self, tint: impl Into) -> Self { - self.image_options.tint = tint.into(); - self - } - - /// Rotate the image about an origin by some angle - /// - /// Positive angle is clockwise. - /// Origin is a vector in normalized UV space ((0,0) in top-left, (1,1) bottom right). - /// - /// To rotate about the center you can pass `Vec2::splat(0.5)` as the origin. - /// - /// Due to limitations in the current implementation, - /// this will turn off rounding of the image. - pub fn rotate(mut self, angle: f32, origin: Vec2) -> Self { - self.image_options.rotation = Some((Rot2::from_angle(angle), origin)); - self.image_options.rounding = Rounding::ZERO; // incompatible with rotation - self - } - - /// Round the corners of the image. - /// - /// The default is no rounding ([`Rounding::ZERO`]). - /// - /// Due to limitations in the current implementation, - /// this will turn off any rotation of the image. - pub fn rounding(mut self, rounding: impl Into) -> Self { - self.image_options.rounding = rounding.into(); - if self.image_options.rounding != Rounding::ZERO { - self.image_options.rotation = None; // incompatible with rounding - } - self - } -} - -impl RawImage { - /// Returns the [`TextureId`] of the texture from which this image was created. - pub fn texture_id(&self) -> TextureId { - self.texture.id - } - - /// Returns the size of the texture from which this image was created. - pub fn size(&self) -> Vec2 { - self.texture.size - } - - pub fn paint_at(&self, ui: &mut Ui, rect: Rect) { - paint_image_at(ui, rect, &self.image_options, &self.texture); - } -} - -impl Widget for RawImage { - fn ui(self, ui: &mut Ui) -> Response { - let (rect, response) = ui.allocate_exact_size(self.size(), self.sense); - self.paint_at(ui, rect); - response - } -} - #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct ImageOptions { @@ -669,7 +663,7 @@ impl Default for ImageOptions { } /// Paint a `SizedTexture` as an image according to some `ImageOptions` at a given `rect`. -pub fn paint_image_at(ui: &mut Ui, rect: Rect, options: &ImageOptions, texture: &SizedTexture) { +pub fn paint_image_at(ui: &Ui, rect: Rect, options: &ImageOptions, texture: &SizedTexture) { if !ui.is_rect_visible(rect) { return; } diff --git a/crates/egui/src/widgets/mod.rs b/crates/egui/src/widgets/mod.rs index 708fcd84..eff7a251 100644 --- a/crates/egui/src/widgets/mod.rs +++ b/crates/egui/src/widgets/mod.rs @@ -22,7 +22,7 @@ pub mod text_edit; pub use button::*; pub use drag_value::DragValue; pub use hyperlink::*; -pub use image::{Image, ImageFit, ImageOptions, ImageSize, ImageSource, RawImage}; +pub use image::{paint_image_at, Image, ImageFit, ImageOptions, ImageSize, ImageSource}; pub use label::*; pub use progress_bar::ProgressBar; pub use selected_label::SelectableLabel; diff --git a/crates/egui/src/widgets/spinner.rs b/crates/egui/src/widgets/spinner.rs index f2b253bc..688c9759 100644 --- a/crates/egui/src/widgets/spinner.rs +++ b/crates/egui/src/widgets/spinner.rs @@ -1,4 +1,4 @@ -use epaint::{emath::lerp, vec2, Color32, Pos2, Shape, Stroke}; +use epaint::{emath::lerp, vec2, Color32, Pos2, Rect, Shape, Stroke}; use crate::{Response, Sense, Ui, Widget}; @@ -31,21 +31,14 @@ impl Spinner { self.color = Some(color.into()); self } -} - -impl Widget for Spinner { - fn ui(self, ui: &mut Ui) -> Response { - let size = self - .size - .unwrap_or_else(|| ui.style().spacing.interact_size.y); - let color = self - .color - .unwrap_or_else(|| ui.visuals().strong_text_color()); - let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover()); + pub fn paint_at(&self, ui: &Ui, rect: Rect) { if ui.is_rect_visible(rect) { ui.ctx().request_repaint(); + let color = self + .color + .unwrap_or_else(|| ui.visuals().strong_text_color()); let radius = (rect.height() / 2.0) - 2.0; let n_points = 20; let time = ui.input(|i| i.time); @@ -61,6 +54,16 @@ impl Widget for Spinner { ui.painter() .add(Shape::line(points, Stroke::new(3.0, color))); } + } +} + +impl Widget for Spinner { + fn ui(self, ui: &mut Ui) -> Response { + let size = self + .size + .unwrap_or_else(|| ui.style().spacing.interact_size.y); + let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover()); + self.paint_at(ui, rect); response } diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 98953961..42251336 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -36,6 +36,7 @@ chrono = { version = "0.4", default-features = false, features = [ eframe = { version = "0.22.0", path = "../eframe", default-features = false } egui = { version = "0.22.0", path = "../egui", features = [ "extra_debug_asserts", + "log", ] } egui_demo_lib = { version = "0.22.0", path = "../egui_demo_lib", features = [ "chrono", @@ -45,8 +46,9 @@ log = { version = "0.4", features = ["std"] } # Optional dependencies: bytemuck = { version = "1.7.1", optional = true } -egui_extras = { version = "0.22.0", optional = true, path = "../egui_extras", features = [ +egui_extras = { version = "0.22.0", path = "../egui_extras", features = [ "log", + "image", ] } rfd = { version = "0.11", optional = true } diff --git a/crates/egui_demo_app/src/apps/http_app.rs b/crates/egui_demo_app/src/apps/http_app.rs index 33da4678..fe43c477 100644 --- a/crates/egui_demo_app/src/apps/http_app.rs +++ b/crates/egui_demo_app/src/apps/http_app.rs @@ -1,6 +1,4 @@ -#![allow(deprecated)] - -use egui_extras::RetainedImage; +use egui::Image; use poll_promise::Promise; struct Resource { @@ -10,7 +8,7 @@ struct Resource { text: Option, /// If set, the response was an image. - image: Option, + image: Option>, /// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md"). colored_text: Option, @@ -19,21 +17,27 @@ struct Resource { impl Resource { fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self { let content_type = response.content_type().unwrap_or_default(); - let image = if content_type.starts_with("image/") { - RetainedImage::from_image_bytes(&response.url, &response.bytes).ok() + if content_type.starts_with("image/") { + ctx.include_bytes(response.url.clone(), response.bytes.clone()); + let image = Image::from_uri(response.url.clone()); + + Self { + response, + text: None, + colored_text: None, + image: Some(image), + } } else { - None - }; + let text = response.text(); + let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text)); + let text = text.map(|text| text.to_owned()); - let text = response.text(); - let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text)); - let text = text.map(|text| text.to_owned()); - - Self { - response, - text, - image, - colored_text, + Self { + response, + text, + colored_text, + image: None, + } } } } @@ -65,6 +69,7 @@ impl eframe::App for HttpApp { }); egui::CentralPanel::default().show(ctx, |ui| { + let prev_url = self.url.clone(); let trigger_fetch = ui_url(ui, frame, &mut self.url); ui.horizontal_wrapped(|ui| { @@ -79,6 +84,7 @@ impl eframe::App for HttpApp { let (sender, promise) = Promise::new(); let request = ehttp::Request::get(&self.url); ehttp::fetch(request, move |response| { + ctx.forget_image(&prev_url); ctx.request_repaint(); // wake up UI thread let resource = response.map(|response| Resource::from_response(&ctx, response)); sender.send(resource); @@ -195,9 +201,7 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) { } if let Some(image) = image { - let mut size = image.size_vec2(); - size *= (ui.available_width() / size.x).min(1.0); - image.show_size(ui, size); + ui.add(image.clone()); } else if let Some(colored_text) = colored_text { colored_text.ui(ui); } else if let Some(text) = &text { diff --git a/crates/egui_demo_app/src/apps/image_viewer.rs b/crates/egui_demo_app/src/apps/image_viewer.rs index f1929b91..98f80f72 100644 --- a/crates/egui_demo_app/src/apps/image_viewer.rs +++ b/crates/egui_demo_app/src/apps/image_viewer.rs @@ -13,7 +13,7 @@ pub struct ImageViewer { chosen_fit: ChosenFit, fit: ImageFit, maintain_aspect_ratio: bool, - max_size: Option, + max_size: Vec2, } #[derive(Clone, Copy, PartialEq, Eq)] @@ -43,7 +43,7 @@ impl Default for ImageViewer { chosen_fit: ChosenFit::Fraction, fit: ImageFit::Fraction(Vec2::splat(1.0)), maintain_aspect_ratio: true, - max_size: None, + max_size: Vec2::splat(2048.0), } } } @@ -160,10 +160,10 @@ impl eframe::App for ImageViewer { ui.add(Slider::new(&mut fract.y, 0.0..=1.0).text("height")); } ChosenFit::OriginalSize => { - if !matches!(self.fit, ImageFit::Original(_)) { - self.fit = ImageFit::Original(Some(1.0)); + if !matches!(self.fit, ImageFit::Original { .. }) { + self.fit = ImageFit::Original { scale: 1.0 }; } - let ImageFit::Original(Some(scale)) = &mut self.fit else { + let ImageFit::Original{scale} = &mut self.fit else { unreachable!() }; ui.add(Slider::new(scale, 0.1..=4.0).text("scale")); @@ -173,21 +173,8 @@ impl eframe::App for ImageViewer { // max size ui.add_space(5.0); ui.label("The calculated size will not exceed the maximum size"); - let had_max_size = self.max_size.is_some(); - let mut has_max_size = had_max_size; - ui.checkbox(&mut has_max_size, "Max size"); - match (had_max_size, has_max_size) { - (true, false) => self.max_size = None, - (false, true) => { - self.max_size = Some(ui.available_size()); - } - (true, true) | (false, false) => {} - } - - if let Some(max_size) = self.max_size.as_mut() { - ui.add(Slider::new(&mut max_size.x, 0.0..=2048.0).text("width")); - ui.add(Slider::new(&mut max_size.y, 0.0..=2048.0).text("height")); - } + ui.add(Slider::new(&mut self.max_size.x, 0.0..=2048.0).text("width")); + ui.add(Slider::new(&mut self.max_size.y, 0.0..=2048.0).text("height")); // aspect ratio ui.add_space(5.0); @@ -209,7 +196,7 @@ impl eframe::App for ImageViewer { }); image = image.rotate(angle, origin); match self.fit { - ImageFit::Original(scale) => image = image.fit_to_original_size(scale), + ImageFit::Original { scale } => image = image.fit_to_original_size(scale), ImageFit::Fraction(fract) => image = image.fit_to_fraction(fract), ImageFit::Exact(size) => image = image.fit_to_exact_size(size), } diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index c1a07e73..07944627 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -165,7 +165,6 @@ pub struct WrapApp { impl WrapApp { pub fn new(_cc: &eframe::CreationContext<'_>) -> Self { - #[cfg(feature = "image_viewer")] egui_extras::loaders::install(&_cc.egui_ctx); #[allow(unused_mut)] diff --git a/crates/egui_demo_lib/assets/icon.png b/crates/egui_demo_lib/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..87f15e746e42df190f84317c76a22f036c50a234 GIT binary patch literal 2642 zcmZ{m2{7B+7sr1hwMOlG5z9l+3KddS38J>zY1LL+v}mlcv^+vlOG`a1Vr}c$T0s#@ zZSg`;f3*DVRM0lmUR8VjW8Tc0`TuABbLZag%sKZn=iIsH&OOOi7DikgVjKVfaG4lm zY#53@oophjcv>U;J!Qn5aI#gfMFsm0YE4m0KT~afOZZ52nXiBwbo%c zSUk*(Fu=*_c-~f)$*{2EP0S5h>FivbGVI^}tz-)TY?CG!J-djhwY(TB=TX5P6us@Z zv$h2d;#7N?gpMO=T4(aq7e6VluyC3tY6s;n?kjJQ^YTPQM`|7VD3xbB=2h zQX|Xh$Of^@DLE3t*F1@Do7q?&qr^njpWtfgZ;w3e@e~#kqNX=8A?AqK$-3~l7W(1( z75YrbM(haku_eMw8^s}zko04RM@6aNt6;w2@8$X9Z!a?ppVJ6vYnXZ|d2EU&19-w6 zQ5xJPs?W?0ef7~=&(2Zlxgh4Cr9c+yV3%@LRt8og%)~`0K_lSYy_XH&m+>(Fg%;mb zsxkMQRgy9FYpG_Q&&=G+0E0Qqcs-k5kdF2#5)?9eA9j7uYYETnzhSMx)`IBQ5COhUvCsXWBgeVxCq7Nh)pWpm4x!(L>@0E{;JqY|YL0%rM z{Olkc5%~4)?v$P#kj>oGlhXzPK}Nv!$v8cf2@lPtuf&0QnAel=$`%Ec>yoYnQ zE2CUpCBtXh)3{J4?8+CnTO>g7iZp^0!m6s5qGFV zfz8fG>)bIIIj!cbd|s%`OxTLK==w$?$Kjnyp) zP66$_MMFmJ?$W!zXovL@I$bLPt;s&QBWr{4y?p{TDye3w74%Nlbp;U!XjAWoaQJLn zVOe2xR(pFi226zys}eqaf={39_#W(*`tU(mU@D5VF9S{@Vy1&$a$W_XwA`CD52K+p z`t;lp%Zn!xVONV4ns(cz>bCZUSMw~~CLmNO@!I#^V$011v%KN&qp!u9_Fi&^EXYVG zpkao;yGXni6CbWcorKE99Q404xpIYfevblJ>K;QfjKKsqcyF$9b>Oh<=QJR@zxzhj zF<*3tL`Xcaf)`o^w8CE$K^Fn*wd`U@>@5v zv({!tG24aVWj}sk;um|;>-@&o>nmR_+%+}$EhG;h35?p)ZCrJ0pG2*Y3NXbVVdAPK z0wh-MQk1Zu;2SqR;vyLNjfFgBSKvgH{ACJrbey)zN>DQd)sNw&)})Eo7J{2M@3%b_ z5FWyTF!8muwUEiFmG*axVzSkAGgThcU8(%ht7aa8Ou-AK@mo2-Yq4%3EPi9o%M;`) zl63x&w8+?x($yoza*5|Uye+cYU|k++lF^^lrqWxrJf*709z81o+LD||f3R#u2HY0b zT{_@2(Qs_RXLlNl4=(b1Ir^XJd{)x?$Y;O=kl9zi}h$1aLW^S)@*y5?G# zo!GBxfZ5!Sa<0e2&XY*>QukB~V80aakgQFmWh_KLR*JsJ$0qDb-FZ>^Hl;gNh{?)X zr^x=b7~AK=<@IHE+8$tFV4zr{?&|2rs2)2TeJZwyp8L9k(h|9wVJzouin$I_uR=zt z{|TDVJ<`P~mXzz1#of~R0eyV~;T<(TIcYMo-gx@hU0hrskO^YZQIym+eWy)_Qb}gOek$ zdG*p5Y*5$3C;*g=evm5dMt? zU$!E2auSoTdfpV;iinHQNyFY0jbu6N##+N7B4SS2GbsrLXN)=XKxLL^B}mdjsTYwV z;^S8_!MsSaFR9p*)rO^wdETGLn&M|McU){Ndi5s0(l?~eoE-`ql3;v#Iqt?| zQ3ps+y~Xjo3yldn*dwe)E2FeOWfe;eN`8w`Nf{S5&Uj-4Mp+HzE2a_oRNRO$E^^2pFd z9uguy9AqbTF{yAF*j5Mw#$A@QzHM%)8X+?`g-wnIOPAi6S7IM0{0~J}rxFLKTM(0e zkD3!IdknJqWyQUcinz*;hPIC<3~lWxQMLJ<{c0X))y33O4aW!khPK7?sw(f`>a72o zQOuK7dgA_xJeFA{ogX&;Cjyy7EJ7(bKbX!mlF>zy{^~pJCocWsR+%?#h}!YkVDvq| z;SC+|Uha6Twr4PwA%Hqu{o;A}rSobR?bOt?FJ9C}Ae7;7Z8#j~vbOqP04~7G$NS#@ z2hIWZRt!M#p9pv#95w{+jtl%>MnfBZ>F*%&$(&yqjOr<45P-vB{qca2$yI1DHpJ&X z)(aZo550rO2Zm^?s$%co_VK>p8Guv$+xhZxQ!NAKJ4I~+yz$}g!C2s*P0y)y4h%0~ MVrYS>(szsd4{n6D`v3p{ literal 0 HcmV?d00001 diff --git a/crates/egui_demo_lib/src/color_test.rs b/crates/egui_demo_lib/src/color_test.rs index 4dc01cc5..ebbb3076 100644 --- a/crates/egui_demo_lib/src/color_test.rs +++ b/crates/egui_demo_lib/src/color_test.rs @@ -88,7 +88,7 @@ impl ColorTest { let texel_offset = 0.5 / (g.0.len() as f32); let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0)); ui.add( - RawImage::new((tex.id(), GRADIENT_SIZE)) + Image::from_texture((tex.id(), GRADIENT_SIZE)) .tint(vertex_color) .uv(uv), ) @@ -230,7 +230,7 @@ impl ColorTest { let texel_offset = 0.5 / (gradient.0.len() as f32); let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0)); ui.add( - RawImage::new((tex.id(), GRADIENT_SIZE)) + Image::from_texture((tex.id(), GRADIENT_SIZE)) .bg_fill(bg_fill) .uv(uv), ) diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index 152d58ac..19cfadfc 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -21,9 +21,6 @@ pub struct WidgetGallery { #[cfg(feature = "chrono")] #[cfg_attr(feature = "serde", serde(skip))] date: Option, - - #[cfg_attr(feature = "serde", serde(skip))] - texture: Option, } impl Default for WidgetGallery { @@ -39,7 +36,6 @@ impl Default for WidgetGallery { animate_progress_bar: false, #[cfg(feature = "chrono")] date: None, - texture: None, } } } @@ -111,14 +107,8 @@ impl WidgetGallery { animate_progress_bar, #[cfg(feature = "chrono")] date, - texture, } = self; - let texture: &egui::TextureHandle = texture.get_or_insert_with(|| { - ui.ctx() - .load_texture("example", egui::ColorImage::example(), Default::default()) - }); - ui.add(doc_link_label("Label", "label,heading")); ui.label("Welcome to the widget gallery!"); ui.end_row(); @@ -206,15 +196,16 @@ impl WidgetGallery { ui.color_edit_button_srgba(color); ui.end_row(); - let img_size = 16.0 * texture.size_vec2() / texture.size_vec2().y; - ui.add(doc_link_label("Image", "Image")); - ui.raw_image((texture.id(), img_size)); + let egui_icon = egui::include_image!("../../assets/icon.png"); + ui.add(egui::Image::new(egui_icon.clone())); ui.end_row(); ui.add(doc_link_label("ImageButton", "ImageButton")); if ui - .add(egui::ImageButton::new((texture.id(), img_size))) + .add(egui::ImageButton::new( + egui::Image::from(egui_icon).max_size(egui::Vec2::splat(16.0)), + )) .clicked() { *boolean = !*boolean; diff --git a/crates/egui_extras/src/image.rs b/crates/egui_extras/src/image.rs index fcc30176..e1d0b330 100644 --- a/crates/egui_extras/src/image.rs +++ b/crates/egui_extras/src/image.rs @@ -191,7 +191,7 @@ impl RetainedImage { // We need to convert the SVG to a texture to display it: // Future improvement: tell backend to do mip-mapping of the image to // make it look smoother when downsized. - ui.raw_image((self.texture_id(ui.ctx()), desired_size)) + ui.image((self.texture_id(ui.ctx()), desired_size)) } } diff --git a/crates/egui_extras/src/lib.rs b/crates/egui_extras/src/lib.rs index 9e918ce1..4850b6f3 100644 --- a/crates/egui_extras/src/lib.rs +++ b/crates/egui_extras/src/lib.rs @@ -13,6 +13,7 @@ #[cfg(feature = "chrono")] mod datepicker; +#[doc(hidden)] pub mod image; mod layout; pub mod loaders; @@ -23,6 +24,7 @@ mod table; #[cfg(feature = "chrono")] pub use crate::datepicker::DatePickerButton; +#[doc(hidden)] #[allow(deprecated)] pub use crate::image::RetainedImage; pub(crate) use crate::layout::StripLayout; diff --git a/crates/egui_glium/examples/native_texture.rs b/crates/egui_glium/examples/native_texture.rs index 94977cd6..eb5a956f 100644 --- a/crates/egui_glium/examples/native_texture.rs +++ b/crates/egui_glium/examples/native_texture.rs @@ -30,8 +30,7 @@ fn main() { egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| { if ui .add(egui::Button::image_and_text( - texture_id, - button_image_size, + (texture_id, button_image_size), "Quit", )) .clicked() diff --git a/crates/egui_plot/src/items/mod.rs b/crates/egui_plot/src/items/mod.rs index aefa6385..7e71de92 100644 --- a/crates/egui_plot/src/items/mod.rs +++ b/crates/egui_plot/src/items/mod.rs @@ -1234,12 +1234,19 @@ impl PlotItem for PlotImage { Rect::from_two_pos(left_top_screen, right_bottom_screen) }; let screen_rotation = -*rotation as f32; - RawImage::new((*texture_id, image_screen_rect.size())) - .bg_fill(*bg_fill) - .tint(*tint) - .uv(*uv) - .rotate(screen_rotation, Vec2::splat(0.5)) - .paint_at(ui, image_screen_rect); + + egui::paint_image_at( + ui, + image_screen_rect, + &ImageOptions { + uv: *uv, + bg_fill: *bg_fill, + tint: *tint, + rotation: Some((Rot2::from_angle(screen_rotation), Vec2::splat(0.5))), + rounding: Rounding::ZERO, + }, + &(*texture_id, image_screen_rect.size()).into(), + ); if *highlight { let center = image_screen_rect.center(); let rotation = Rot2::from_angle(screen_rotation); diff --git a/examples/images/src/main.rs b/examples/images/src/main.rs index 193ab37c..656456cb 100644 --- a/examples/images/src/main.rs +++ b/examples/images/src/main.rs @@ -32,7 +32,7 @@ impl eframe::App for MyApp { .fit_to_fraction(vec2(1.0, 0.5)), ); ui.add( - egui::Image::new("https://picsum.photos/seed/1.759706314/1024".into()) + egui::Image::new("https://picsum.photos/seed/1.759706314/1024") .rounding(egui::Rounding::same(10.0)), ); }); diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 9533a425..498d5013 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -63,7 +63,7 @@ impl eframe::App for MyApp { }); if let Some(texture) = self.texture.as_ref() { - ui.raw_image((texture.id(), ui.available_size())); + ui.image((texture.id(), ui.available_size())); } else { ui.spinner(); }