From d7d222d3f6c6d7f5b504195f06ecfe81ede129d6 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 14 Sep 2023 16:33:10 +0200 Subject: [PATCH] Polish image API (#3338) * Imoprove docs for callback shapes * Improve docs for loader traits * Use snake_case for feature `all_loaders` * Make loaders publix * Slightly better error message on image load failure * Improve image loading error messages * Use `bytes://` schema for included bytes loader * Try user loaders first * Move `image_loading_spinners` to `Visuals` * Unify and simplify code * Make the main text of `Button` optional This largely makes ImageButton obsolete * Fix docstrings * Better docs * typos * Use the more explicit `egui_extras::install_image_loaders` * Simplify `Image::paint_at` function --- crates/egui-wgpu/src/renderer.rs | 29 +-- crates/egui/src/context.rs | 78 +++--- crates/egui/src/lib.rs | 5 +- crates/egui/src/load.rs | 66 +++-- crates/egui/src/load/bytes_loader.rs | 14 +- crates/egui/src/style.rs | 18 +- crates/egui/src/ui.rs | 11 +- crates/egui/src/widgets/button.rs | 227 +++++++++++------- crates/egui/src/widgets/image.rs | 105 ++++---- crates/egui/src/widgets/spinner.rs | 1 + crates/egui_demo_app/Cargo.toml | 2 +- crates/egui_demo_app/src/wrap_app.rs | 2 +- .../egui_demo_lib/src/demo/widget_gallery.rs | 9 +- crates/egui_extras/Cargo.toml | 16 +- crates/egui_extras/src/lib.rs | 2 + crates/egui_extras/src/loaders.rs | 26 +- .../egui_extras/src/loaders/ehttp_loader.rs | 2 +- crates/egui_extras/src/loaders/file_loader.rs | 2 +- .../egui_extras/src/loaders/image_loader.rs | 4 +- crates/egui_extras/src/loaders/svg_loader.rs | 4 +- crates/egui_glow/src/painter.rs | 29 +-- crates/epaint/src/shape.rs | 10 +- crates/epaint/src/tessellator.rs | 2 +- examples/images/Cargo.toml | 2 +- examples/images/src/main.rs | 11 +- 25 files changed, 402 insertions(+), 275 deletions(-) diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index 532c7c01413..62d9698332a 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -445,6 +445,13 @@ impl Renderer { needs_reset = true; + let info = PaintCallbackInfo { + viewport: callback.rect, + clip_rect: *clip_rect, + pixels_per_point, + screen_size_px: size_in_pixels, + }; + { // We're setting a default viewport for the render pass as a // courtesy for the user, so that they don't have to think about @@ -455,29 +462,19 @@ impl Renderer { // viewport during the paint callback, effectively overriding this // one. - let min = (callback.rect.min.to_vec2() * pixels_per_point).round(); - let max = (callback.rect.max.to_vec2() * pixels_per_point).round(); + let viewport_px = info.viewport_in_pixels(); render_pass.set_viewport( - min.x, - min.y, - max.x - min.x, - max.y - min.y, + viewport_px.left_px, + viewport_px.top_px, + viewport_px.width_px, + viewport_px.height_px, 0.0, 1.0, ); } - cbfn.0.paint( - PaintCallbackInfo { - viewport: callback.rect, - clip_rect: *clip_rect, - pixels_per_point, - screen_size_px: size_in_pixels, - }, - render_pass, - &self.callback_resources, - ); + cbfn.0.paint(info, render_pass, &self.callback_resources); } } } diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index aca9d2baa48..f87ce43b6c5 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1121,9 +1121,16 @@ impl Context { /// Allocate a texture. /// + /// This is for advanced users. + /// Most users should use [`crate::Ui::image`] or [`Self::try_load_texture`] + /// instead. + /// /// In order to display an image you must convert it to a texture using this function. + /// The function will hand over the image data to the egui backend, which will + /// upload it to the GPU. /// - /// Make sure to only call this once for each image, i.e. NOT in your main GUI code. + /// ⚠️ Make sure to only call this ONCE for each image, i.e. NOT in your main GUI code. + /// The call is NOT immediate safe. /// /// The given name can be useful for later debugging, and will be visible if you call [`Self::texture_ui`]. /// @@ -1151,7 +1158,7 @@ impl Context { /// } /// ``` /// - /// Se also [`crate::ImageData`], [`crate::Ui::image`] and [`crate::ImageButton`]. + /// See also [`crate::ImageData`], [`crate::Ui::image`] and [`crate::Image`]. pub fn load_texture( &self, name: impl Into, @@ -1912,6 +1919,9 @@ 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. + /// + /// By convention, the `uri` should start with `bytes://`. + /// Following that convention will lead to better error messages. pub fn include_bytes(&self, uri: impl Into>, bytes: impl Into) { self.loaders().include.insert(uri, bytes); } @@ -1921,32 +1931,32 @@ impl Context { pub fn is_loader_installed(&self, id: &str) -> bool { let loaders = self.loaders(); - let in_bytes = loaders.bytes.lock().iter().any(|loader| loader.id() == id); - let in_image = loaders.image.lock().iter().any(|loader| loader.id() == id); - let in_texture = loaders - .texture - .lock() - .iter() - .any(|loader| loader.id() == id); - - in_bytes || in_image || in_texture + loaders.bytes.lock().iter().any(|l| l.id() == id) + || loaders.image.lock().iter().any(|l| l.id() == id) + || loaders.texture.lock().iter().any(|l| l.id() == id) } - /// Append an entry onto the chain of bytes loaders. + /// Add a new bytes loader. + /// + /// It will be tried first, before any already installed loaders. /// /// See [`load`] for more information. pub fn add_bytes_loader(&self, loader: Arc) { self.loaders().bytes.lock().push(loader); } - /// Append an entry onto the chain of image loaders. + /// Add a new image loader. + /// + /// It will be tried first, before any already installed loaders. /// /// See [`load`] for more information. pub fn add_image_loader(&self, loader: Arc) { self.loaders().image.lock().push(loader); } - /// Append an entry onto the chain of texture loaders. + /// Add a new texture loader. + /// + /// It will be tried first, before any already installed loaders. /// /// See [`load`] for more information. pub fn add_texture_loader(&self, loader: Arc) { @@ -2009,23 +2019,27 @@ impl Context { /// # Errors /// This may fail with: /// - [`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. + /// - [`LoadError::Loading`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed. /// /// ⚠ May deadlock if called from within a `BytesLoader`! /// /// [not_supported]: crate::load::LoadError::NotSupported - /// [custom]: crate::load::LoadError::Custom + /// [custom]: crate::load::LoadError::Loading pub fn try_load_bytes(&self, uri: &str) -> load::BytesLoadResult { crate::profile_function!(); - for loader in self.loaders().bytes.lock().iter() { + let loaders = self.loaders(); + let bytes_loaders = loaders.bytes.lock(); + + // Try most recently added loaders first (hence `.rev()`) + for loader in bytes_loaders.iter().rev() { match loader.load(self, uri) { Err(load::LoadError::NotSupported) => continue, result => return result, } } - Err(load::LoadError::NotSupported) + Err(load::LoadError::NoMatchingBytesLoader) } /// Try loading the image from the given uri using any available image loaders. @@ -2041,30 +2055,31 @@ impl Context { /// 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. + /// - [`LoadError::Loading`][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 + /// [custom]: crate::load::LoadError::Loading pub fn try_load_image(&self, uri: &str, size_hint: load::SizeHint) -> load::ImageLoadResult { crate::profile_function!(); let loaders = self.loaders(); - let loaders = loaders.image.lock(); - if loaders.is_empty() { + let image_loaders = loaders.image.lock(); + if image_loaders.is_empty() { return Err(load::LoadError::NoImageLoaders); } - for loader in loaders.iter() { + // Try most recently added loaders first (hence `.rev()`) + for loader in image_loaders.iter().rev() { match loader.load(self, uri, size_hint) { Err(load::LoadError::NotSupported) => continue, result => return result, } } - Err(load::LoadError::NotSupported) + Err(load::LoadError::NoMatchingImageLoader) } /// Try loading the texture from the given uri using any available texture loaders. @@ -2079,12 +2094,12 @@ impl Context { /// # Errors /// This may fail with: /// - [`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. + /// - [`LoadError::Loading`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed. /// /// ⚠ May deadlock if called from within a `TextureLoader`! /// /// [not_supported]: crate::load::LoadError::NotSupported - /// [custom]: crate::load::LoadError::Custom + /// [custom]: crate::load::LoadError::Loading pub fn try_load_texture( &self, uri: &str, @@ -2093,17 +2108,22 @@ impl Context { ) -> load::TextureLoadResult { crate::profile_function!(); - for loader in self.loaders().texture.lock().iter() { + let loaders = self.loaders(); + let texture_loaders = loaders.texture.lock(); + + // Try most recently added loaders first (hence `.rev()`) + for loader in texture_loaders.iter().rev() { match loader.load(self, uri, texture_options, size_hint) { Err(load::LoadError::NotSupported) => continue, result => return result, } } - Err(load::LoadError::NotSupported) + Err(load::LoadError::NoMatchingTextureLoader) } - fn loaders(&self) -> Arc { + /// The loaders of bytes, images, and textures. + pub fn loaders(&self) -> Arc { crate::profile_function!(); self.read(|this| this.loaders.clone()) } diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 4bc2dbbfc8f..68d79807554 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -437,13 +437,16 @@ pub fn warn_if_debug_build(ui: &mut crate::Ui) { /// egui::Image::new(egui::include_image!("../assets/ferris.png")) /// .rounding(egui::Rounding::same(6.0)) /// ); +/// +/// let image_source: egui::ImageSource = egui::include_image!("../assets/ferris.png"); +/// assert_eq!(image_source.uri(), Some("bytes://../assets/ferris.png")); /// # }); /// ``` #[macro_export] macro_rules! include_image { ($path: literal) => { $crate::ImageSource::Bytes( - ::std::borrow::Cow::Borrowed($path), + ::std::borrow::Cow::Borrowed(concat!("bytes://", $path)), // uri $crate::load::Bytes::Static(include_bytes!($path)), ) }; diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs index 2bd6c63225b..6f277cb5eb0 100644 --- a/crates/egui/src/load.rs +++ b/crates/egui/src/load.rs @@ -3,8 +3,8 @@ //! If you just want to display some images, [`egui_extras`](https://crates.io/crates/egui_extras/) //! will get you up and running quickly with its reasonable default implementations of the traits described below. //! -//! 1. Add [`egui_extras`](https://crates.io/crates/egui_extras/) as a dependency with the `all-loaders` feature. -//! 2. Add a call to [`egui_extras::loaders::install`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html) +//! 1. Add [`egui_extras`](https://crates.io/crates/egui_extras/) as a dependency with the `all_loaders` feature. +//! 2. Add a call to [`egui_extras::install_image_loaders`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html) //! in your app's setup code. //! 3. Use [`Ui::image`][`crate::ui::Ui::image`] with some [`ImageSource`][`crate::ImageSource`]. //! @@ -55,8 +55,6 @@ 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; @@ -69,27 +67,50 @@ use std::fmt::Debug; use std::ops::Deref; use std::{error::Error as StdError, fmt::Display, sync::Arc}; +pub use self::bytes_loader::DefaultBytesLoader; +pub use self::texture_loader::DefaultTextureLoader; + /// Represents a failed attempt at loading an image. #[derive(Clone, Debug)] pub enum LoadError { - /// There are no image loaders installed. + /// Programmer error: There are no image loaders installed. NoImageLoaders, - /// This loader does not support this protocol or image format. + /// A specific loader does not support this schema, protocol or image format. NotSupported, - /// A custom error message (e.g. "File not found: foo.png"). - Custom(String), + /// Programmer error: Failed to find the bytes for this image because + /// there was no [`BytesLoader`] supporting the schema. + NoMatchingBytesLoader, + + /// Programmer error: Failed to parse the bytes as an image because + /// there was no [`ImageLoader`] supporting the schema. + NoMatchingImageLoader, + + /// Programmer error: no matching [`TextureLoader`]. + /// Because of the [`DefaultTextureLoader`], this error should never happen. + NoMatchingTextureLoader, + + /// Runtime error: Loading was attempted, but failed (e.g. "File not found"). + Loading(String), } impl Display for LoadError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - LoadError::NoImageLoaders => f.write_str( + Self::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), + + Self::NoMatchingBytesLoader => f.write_str("No matching BytesLoader. Either you need to call Context::include_bytes, or install some more bytes loaders, e.g. using egui_extras."), + + Self::NoMatchingImageLoader => f.write_str("No matching ImageLoader. Either you need to call Context::include_bytes, or install some more bytes loaders, e.g. using egui_extras."), + + Self::NoMatchingTextureLoader => f.write_str("No matching TextureLoader. Did you remove the default one?"), + + Self::NotSupported => f.write_str("Iagge schema or URI not supported by this loader"), + + Self::Loading(message) => f.write_str(message), } } } @@ -238,7 +259,8 @@ pub use crate::generate_loader_id; pub type BytesLoadResult = Result; -/// Represents a loader capable of loading raw unstructured bytes. +/// Represents a loader capable of loading raw unstructured bytes from somewhere, +/// e.g. from disk or network. /// /// It should also provide any subsequent loaders a hint for what the bytes may /// represent using [`BytesPoll::Ready::mime`], if it can be inferred. @@ -261,7 +283,7 @@ pub trait BytesLoader { /// # Errors /// This may fail with: /// - [`LoadError::NotSupported`] if the loader does not support loading `uri`. - /// - [`LoadError::Custom`] if the loading process failed. + /// - [`LoadError::Loading`] if the loading process failed. fn load(&self, ctx: &Context, uri: &str) -> BytesLoadResult; /// Forget the given `uri`. @@ -305,7 +327,7 @@ pub enum ImagePoll { pub type ImageLoadResult = Result; -/// Represents a loader capable of loading a raw image. +/// An `ImageLoader` decodes raw bytes into a [`ColorImage`]. /// /// Implementations are expected to cache at least each `URI`. pub trait ImageLoader { @@ -328,7 +350,7 @@ pub trait ImageLoader { /// # Errors /// This may fail with: /// - [`LoadError::NotSupported`] if the loader does not support loading `uri`. - /// - [`LoadError::Custom`] if the loading process failed. + /// - [`LoadError::Loading`] if the loading process failed. fn load(&self, ctx: &Context, uri: &str, size_hint: SizeHint) -> ImageLoadResult; /// Forget the given `uri`. @@ -420,7 +442,14 @@ impl TexturePoll { pub type TextureLoadResult = Result; -/// Represents a loader capable of loading a full texture. +/// A `TextureLoader` uploads a [`ColorImage`] to the GPU, returning a [`SizedTexture`]. +/// +/// `egui` comes with an implementation that uses [`Context::load_texture`], +/// which just asks the egui backend to upload the image to the GPU. +/// +/// You can implement this trait if you do your own uploading of images to the GPU. +/// For instance, you can use this to refer to textures in a game engine that egui +/// doesn't otherwise know about. /// /// Implementations are expected to cache each combination of `(URI, TextureOptions)`. pub trait TextureLoader { @@ -443,7 +472,7 @@ pub trait TextureLoader { /// # Errors /// This may fail with: /// - [`LoadError::NotSupported`] if the loader does not support loading `uri`. - /// - [`LoadError::Custom`] if the loading process failed. + /// - [`LoadError::Loading`] if the loading process failed. fn load( &self, ctx: &Context, @@ -479,7 +508,8 @@ type ImageLoaderImpl = Arc; type TextureLoaderImpl = Arc; #[derive(Clone)] -pub(crate) struct Loaders { +/// The loaders of bytes, images, and textures. +pub struct Loaders { pub include: Arc, pub bytes: Mutex>, pub image: Mutex>, diff --git a/crates/egui/src/load/bytes_loader.rs b/crates/egui/src/load/bytes_loader.rs index 062011c5589..3ab46794912 100644 --- a/crates/egui/src/load/bytes_loader.rs +++ b/crates/egui/src/load/bytes_loader.rs @@ -1,5 +1,8 @@ use super::*; +/// Maps URI:s to [`Bytes`], e.g. found with `include_bytes!`. +/// +/// By convention, the URI:s should be prefixed with `bytes://`. #[derive(Default)] pub struct DefaultBytesLoader { cache: Mutex, Bytes>>, @@ -27,13 +30,22 @@ impl BytesLoader for DefaultBytesLoader { } fn load(&self, _: &Context, uri: &str) -> BytesLoadResult { + // We accept uri:s that don't start with `bytes://` too… for now. match self.cache.lock().get(uri).cloned() { Some(bytes) => Ok(BytesPoll::Ready { size: None, bytes, mime: None, }), - None => Err(LoadError::NotSupported), + None => { + if uri.starts_with("bytes://") { + Err(LoadError::Loading( + "Bytes not found. Did you forget to call Context::include_bytes?".into(), + )) + } else { + Err(LoadError::NotSupported) + } + } } } diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 69185dbe916..cc31a1c617b 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -207,9 +207,6 @@ 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 { @@ -556,6 +553,9 @@ pub struct Visuals { /// all turn your cursor into [`CursorIcon::PointingHand`] when a button is /// hovered) but it is inconsistent with native UI toolkits. pub interact_cursor: Option, + + /// Show a spinner when loading an image. + pub image_loading_spinners: bool, } impl Visuals { @@ -741,7 +741,6 @@ impl Default for Style { animation_time: 1.0 / 12.0, debug: Default::default(), explanation_tooltips: false, - image_loading_spinners: true, } } } @@ -821,6 +820,8 @@ impl Visuals { slider_trailing_fill: false, interact_cursor: None, + + image_loading_spinners: true, } } @@ -994,7 +995,6 @@ impl Style { animation_time, debug, explanation_tooltips, - image_loading_spinners, } = self; visuals.light_dark_radio_buttons(ui); @@ -1062,9 +1062,6 @@ 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)); } } @@ -1396,6 +1393,8 @@ impl Visuals { slider_trailing_fill, interact_cursor, + + image_loading_spinners, } = self; ui.collapsing("Background Colors", |ui| { @@ -1471,6 +1470,9 @@ impl Visuals { } }); + 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 4886e2d7b45..01241ccf2fd 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -1561,7 +1561,7 @@ impl Ui { /// Show an image available at the given `uri`. /// /// ⚠ This will do nothing unless you install some image loaders first! - /// The easiest way to do this is via [`egui_extras::loaders::install`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html). + /// The easiest way to do this is via [`egui_extras::install_image_loaders`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html). /// /// The loaders handle caching image data, sampled textures, etc. across frames, so calling this is immediate-mode safe. /// @@ -1577,9 +1577,10 @@ impl Ui { /// # }); /// ``` /// - /// Note: Prefer `include_image` as a source if you're loading an image - /// from a file with a statically known path, unless you really want to - /// load it at runtime instead! + /// Using [`include_image`] is often the most ergonomic, and the path + /// will be resolved at compile-time and embedded in the binary. + /// When using a "file://" url on the other hand, you need to make sure + /// the files can be found in the right spot at runtime! /// /// See also [`crate::Image`], [`crate::ImageSource`]. #[inline] @@ -1687,7 +1688,7 @@ impl Ui { /// # }); /// ``` /// - /// Se also [`Self::scope`]. + /// See also [`Self::scope`]. pub fn group(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse { crate::Frame::group(self.style()).show(self, add_contents) } diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 6c834fd8a35..b67f8d491a0 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -1,5 +1,3 @@ -use crate::load::SizedTexture; -use crate::load::TexturePoll; use crate::*; /// Clickable button with text. @@ -22,7 +20,8 @@ use crate::*; /// ``` #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] pub struct Button<'a> { - text: WidgetText, + image: Option>, + text: Option, shortcut_text: WidgetText, wrap: Option, @@ -34,13 +33,30 @@ pub struct Button<'a> { frame: Option, min_size: Vec2, rounding: Option, - image: Option>, + selected: bool, } impl<'a> Button<'a> { pub fn new(text: impl Into) -> Self { + Self::opt_image_and_text(None, Some(text.into())) + } + + /// Creates a button with an image. The size of the image as displayed is defined by the provided size. + #[allow(clippy::needless_pass_by_value)] + pub fn image(image: impl Into>) -> Self { + Self::opt_image_and_text(Some(image.into()), None) + } + + /// 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(image: impl Into>, text: impl Into) -> Self { + Self::opt_image_and_text(Some(image.into()), Some(text.into())) + } + + pub fn opt_image_and_text(image: Option>, text: Option) -> Self { Self { - text: text.into(), + text, + image, shortcut_text: Default::default(), wrap: None, fill: None, @@ -50,16 +66,7 @@ impl<'a> Button<'a> { frame: None, min_size: Vec2::ZERO, rounding: None, - image: None, - } - } - - /// 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(image: impl Into>, text: impl Into) -> Self { - Self { - image: Some(image.into()), - ..Self::new(text) + selected: false, } } @@ -94,7 +101,9 @@ impl<'a> Button<'a> { /// Make this a small button, suitable for embedding into text. pub fn small(mut self) -> Self { - self.text = self.text.text_style(TextStyle::Body); + if let Some(text) = self.text { + self.text = Some(text.text_style(TextStyle::Body)); + } self.small = true; self } @@ -133,12 +142,19 @@ impl<'a> Button<'a> { self.shortcut_text = shortcut_text.into(); self } + + /// If `true`, mark this button as "selected". + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } } impl Widget for Button<'_> { fn ui(self, ui: &mut Ui) -> Response { let Button { text, + image, shortcut_text, wrap, fill, @@ -148,21 +164,35 @@ impl Widget for Button<'_> { frame, min_size, rounding, - image, + selected, } = 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; + let mut button_padding = if frame { + ui.spacing().button_padding + } else { + Vec2::ZERO + }; if small { button_padding.y = 0.0; } + let space_available_for_image = if let Some(text) = &text { + let font_height = ui.fonts(|fonts| text.font_height(fonts, ui.style())); + Vec2::splat(font_height) // Reasonable? + } else { + ui.available_size() - 2.0 * button_padding + }; + + let image_size = if let Some(image) = &image { + image + .load_and_calculate_size(ui, space_available_for_image) + .unwrap_or(space_available_for_image) + } else { + Vec2::ZERO + }; + let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x; if image.is_some() { text_wrap_width -= image_size.x + ui.spacing().icon_spacing; @@ -171,15 +201,22 @@ impl Widget for Button<'_> { text_wrap_width -= 60.0; // Some space for the shortcut text (which we never wrap). } - let text = text.into_galley(ui, wrap, text_wrap_width, TextStyle::Button); + let text = text.map(|text| text.into_galley(ui, wrap, text_wrap_width, TextStyle::Button)); let shortcut_text = (!shortcut_text.is_empty()) .then(|| shortcut_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button)); - let mut desired_size = text.size(); + let mut desired_size = Vec2::ZERO; if image.is_some() { - desired_size.x += image_size.x + ui.spacing().icon_spacing; + desired_size.x += image_size.x; desired_size.y = desired_size.y.max(image_size.y); } + if image.is_some() && text.is_some() { + desired_size.x += ui.spacing().icon_spacing; + } + if let Some(text) = &text { + desired_size.x += text.size().x; + desired_size.y = desired_size.y.max(text.size().y); + } if let Some(shortcut_text) = &shortcut_text { desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x; desired_size.y = desired_size.y.max(shortcut_text.size().y); @@ -191,66 +228,93 @@ impl Widget for Button<'_> { desired_size = desired_size.at_least(min_size); let (rect, mut response) = ui.allocate_at_least(desired_size, sense); - response.widget_info(|| WidgetInfo::labeled(WidgetType::Button, text.text())); + response.widget_info(|| { + if let Some(text) = &text { + WidgetInfo::labeled(WidgetType::Button, text.text()) + } else { + WidgetInfo::new(WidgetType::Button) + } + }); if ui.is_rect_visible(rect) { let visuals = ui.style().interact(&response); - if frame { - let fill = fill.unwrap_or(visuals.weak_bg_fill); - let stroke = stroke.unwrap_or(visuals.bg_stroke); - let rounding = rounding.unwrap_or(visuals.rounding); - ui.painter() - .rect(rect.expand(visuals.expansion), rounding, fill, stroke); - } - - 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.center().y - 0.5 * text.size().y, + let (frame_expansion, frame_rounding, frame_fill, frame_stroke) = if selected { + let selection = ui.visuals().selection; + ( + Vec2::ZERO, + Rounding::ZERO, + selection.bg_fill, + selection.stroke, + ) + } else if frame { + let expansion = Vec2::splat(visuals.expansion); + ( + expansion, + visuals.rounding, + visuals.weak_bg_fill, + visuals.bg_stroke, ) } else { - ui.layout() - .align_size_within_rect(text.size(), rect.shrink2(button_padding)) - .min + Default::default() }; - text.paint_with_visuals(ui.painter(), text_pos, visuals); - - if let Some(shortcut_text) = shortcut_text { - let shortcut_text_pos = pos2( - rect.max.x - button_padding.x - shortcut_text.size().x, - rect.center().y - 0.5 * shortcut_text.size().y, - ); - shortcut_text.paint_with_fallback_color( - ui.painter(), - shortcut_text_pos, - ui.visuals().weak_text_color(), - ); - } + let frame_rounding = rounding.unwrap_or(frame_rounding); + let frame_fill = fill.unwrap_or(frame_fill); + let frame_stroke = stroke.unwrap_or(frame_stroke); + ui.painter().rect( + rect.expand2(frame_expansion), + frame_rounding, + frame_fill, + frame_stroke, + ); + + let mut cursor_x = rect.min.x + button_padding.x; if let Some(image) = &image { let image_rect = Rect::from_min_size( - pos2( - rect.min.x + button_padding.x, - rect.center().y - 0.5 - (image_size.y / 2.0), - ), + pos2(cursor_x, rect.center().y - 0.5 - (image_size.y / 2.0)), image_size, ); + cursor_x += image_size.x; 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.show_loading_spinner, image.image_options(), ); response = widgets::image::texture_load_result_response(image.source(), &tlr, response); } + + if image.is_some() && text.is_some() { + cursor_x += ui.spacing().icon_spacing; + } + + if let Some(text) = text { + let text_pos = if image.is_some() || shortcut_text.is_some() { + pos2(cursor_x, rect.center().y - 0.5 * text.size().y) + } else { + // Make sure button text is centered if within a centered layout + ui.layout() + .align_size_within_rect(text.size(), rect.shrink2(button_padding)) + .min + }; + text.paint_with_visuals(ui.painter(), text_pos, visuals); + } + + if let Some(shortcut_text) = shortcut_text { + let shortcut_text_pos = pos2( + rect.max.x - button_padding.x - shortcut_text.size().x, + rect.center().y - 0.5 * shortcut_text.size().y, + ); + shortcut_text.paint_with_fallback_color( + ui.painter(), + shortcut_text_pos, + ui.visuals().weak_text_color(), + ); + } } if let Some(cursor) = ui.visuals().interact_cursor { @@ -530,8 +594,10 @@ impl<'a> ImageButton<'a> { self.sense = sense; self } +} - fn show(&self, ui: &mut Ui, texture: &SizedTexture) -> Response { +impl<'a> Widget for ImageButton<'a> { + fn ui(self, ui: &mut Ui) -> Response { let padding = if self.frame { // so we can see that it is a button: Vec2::splat(ui.spacing().button_padding.x) @@ -539,7 +605,13 @@ impl<'a> ImageButton<'a> { Vec2::ZERO }; - let padded_size = texture.size + 2.0 * padding; + let tlr = self.image.load(ui); + let texture_size = tlr.as_ref().ok().and_then(|t| t.size()); + let image_size = self + .image + .calculate_size(ui.available_size() - 2.0 * padding, texture_size); + + let padded_size = image_size + 2.0 * padding; let (rect, response) = ui.allocate_exact_size(padded_size, self.sense); response.widget_info(|| WidgetInfo::new(WidgetType::ImageButton)); @@ -571,36 +643,19 @@ impl<'a> ImageButton<'a> { let image_rect = ui .layout() - .align_size_within_rect(texture.size, rect.shrink2(padding)); + .align_size_within_rect(image_size, rect.shrink2(padding)); // let image_rect = image_rect.expand2(expansion); // can make it blurry, so let's not let image_options = ImageOptions { rounding, // apply rounding to the image ..self.image.image_options().clone() }; - crate::widgets::image::paint_image_at(ui, image_rect, &image_options, texture); + widgets::image::paint_texture_load_result(ui, &tlr, image_rect, None, &image_options); // Draw frame outline: ui.painter() .rect_stroke(rect.expand2(expansion), rounding, stroke); } - response - } -} - -impl<'a> Widget for ImageButton<'a> { - fn ui(self, ui: &mut Ui) -> Response { - match self.image.load(ui) { - 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())), - Err(err) => ui - .colored_label(ui.visuals().error_fg_color, "⚠") - .on_hover_text(err.to_string()), - } + widgets::image::texture_load_result_response(self.image.source(), &tlr, response) } } diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index 163e264e492..71c77decb74 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -74,6 +74,8 @@ impl<'a> Image<'a> { /// Load the image from some raw bytes. /// + /// For better error messages, use the `bytes://` prefix for the URI. + /// /// See [`ImageSource::Bytes`]. pub fn from_bytes(uri: impl Into>, bytes: impl Into) -> Self { Self::new(ImageSource::Bytes(uri.into(), bytes.into())) @@ -220,7 +222,7 @@ impl<'a> Image<'a> { /// Show a spinner when the image is loading. /// - /// By default this uses the value of [`Style::image_loading_spinners`]. + /// By default this uses the value of [`Visuals::image_loading_spinners`]. #[inline] pub fn show_loading_spinner(mut self, show: bool) -> Self { self.show_loading_spinner = Some(show); @@ -237,7 +239,8 @@ impl<'a, T: Into>> From for Image<'a> { 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 { + pub fn calculate_size(&self, available_size: Vec2, image_size: Option) -> Vec2 { + let image_size = image_size.unwrap_or(Vec2::splat(24.0)); // Fallback for still-loading textures, or failure to load. self.size.get(available_size, image_size) } @@ -264,18 +267,9 @@ impl<'a> Image<'a> { &self.source } - /// Get the `uri` that this image was constructed from. - /// - /// This will return `` for [`ImageSource::Texture`]. - #[inline] - pub fn uri(&self) -> &str { - self.source.uri().unwrap_or("") - } - /// Load the image from its [`Image::source`], returning the resulting [`SizedTexture`]. /// /// # Errors - /// /// May fail if they underlying [`Context::try_load_texture`] call fails. pub fn load(&self, ui: &Ui) -> TextureLoadResult { let size_hint = self.size.hint(ui.available_size()); @@ -284,41 +278,34 @@ impl<'a> Image<'a> { .load(ui.ctx(), self.texture_options, size_hint) } + /// Paint the image in the given rectangle. #[inline] - pub fn paint_at(&self, ui: &mut Ui, rect: Rect, texture: &SizedTexture) { - paint_image_at(ui, rect, &self.image_options, texture); + pub fn paint_at(&self, ui: &mut Ui, rect: Rect) { + paint_texture_load_result( + ui, + &self.load(ui), + rect, + self.show_loading_spinner, + &self.image_options, + ); } } impl<'a> Widget for Image<'a> { fn ui(self, ui: &mut Ui) -> Response { - match self.load(ui) { - 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())) - } - } - } - Err(err) => ui - .colored_label(ui.visuals().error_fg_color, "⚠") - .on_hover_text(err.to_string()), - } + let tlr = self.load(ui); + let texture_size = tlr.as_ref().ok().and_then(|t| t.size()); + let ui_size = self.calculate_size(ui.available_size(), texture_size); + + let (rect, response) = ui.allocate_exact_size(ui_size, self.sense); + paint_texture_load_result( + ui, + &tlr, + rect, + self.show_loading_spinner, + &self.image_options, + ); + texture_load_result_response(&self.source, &tlr, response) } } @@ -353,12 +340,16 @@ pub struct ImageSize { #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum ImageFit { /// Fit the image to its original size, scaled by some factor. + /// + /// Ignores how much space is actually available in the ui. Original { scale: f32 }, /// Fit the image to a fraction of the available size. Fraction(Vec2), /// Fit the image to an exact size. + /// + /// Ignores how much space is actually available in the ui. Exact(Vec2), } @@ -373,7 +364,7 @@ impl ImageFit { } impl ImageSize { - fn hint(&self, available_size: Vec2) -> SizeHint { + pub fn hint(&self, available_size: Vec2) -> SizeHint { if self.maintain_aspect_ratio { return SizeHint::Scale(1.0.ord()); }; @@ -395,7 +386,7 @@ impl ImageSize { } } - fn get(&self, available_size: Vec2, image_size: Vec2) -> Vec2 { + pub fn get(&self, available_size: Vec2, image_size: Vec2) -> Vec2 { let Self { maintain_aspect_ratio, max_size, @@ -449,7 +440,7 @@ impl Default for ImageSize { /// This type tells the [`Ui`] how to load an image. /// /// This is used by [`Image::new`] and [`Ui::image`]. -#[derive(Debug, Clone)] +#[derive(Clone)] pub enum ImageSource<'a> { /// Load the image from a URI. /// @@ -468,6 +459,8 @@ pub enum ImageSource<'a> { /// Load the image from some raw bytes. /// + /// For better error messages, use the `bytes://` prefix for the URI. + /// /// The [`Bytes`] may be: /// - `'static`, obtained from `include_bytes!` or similar /// - Anything that can be converted to `Arc<[u8]>` @@ -480,6 +473,15 @@ pub enum ImageSource<'a> { Bytes(Cow<'static, str>, Bytes), } +impl<'a> std::fmt::Debug for ImageSource<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ImageSource::Bytes(uri, _) | ImageSource::Uri(uri) => uri.as_ref().fmt(f), + ImageSource::Texture(st) => st.id.fmt(f), + } + } +} + impl<'a> ImageSource<'a> { /// # Errors /// Failure to load the texture. @@ -514,7 +516,7 @@ pub fn paint_texture_load_result( ui: &Ui, tlr: &TextureLoadResult, rect: Rect, - show_loading_spinner: bool, + show_loading_spinner: Option, options: &ImageOptions, ) { match tlr { @@ -522,6 +524,8 @@ pub fn paint_texture_load_result( paint_image_at(ui, rect, options, texture); } Ok(TexturePoll::Pending { .. }) => { + let show_loading_spinner = + show_loading_spinner.unwrap_or(ui.visuals().image_loading_spinners); if show_loading_spinner { Spinner::new().paint_at(ui, rect); } @@ -539,6 +543,7 @@ pub fn paint_texture_load_result( } } +/// Attach tooltips like "Loading…" or "Failed loading: …". pub fn texture_load_result_response( source: &ImageSource<'_>, tlr: &TextureLoadResult, @@ -547,13 +552,13 @@ pub fn texture_load_result_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…") - } + let uri = source.uri().unwrap_or("image"); + response.on_hover_text(format!("Loading {uri}…")) + } + Err(err) => { + let uri = source.uri().unwrap_or("image"); + response.on_hover_text(format!("Failed loading {uri}: {err}")) } - Err(err) => response.on_hover_text(err.to_string()), } } diff --git a/crates/egui/src/widgets/spinner.rs b/crates/egui/src/widgets/spinner.rs index 688c9759eda..e962810f8e3 100644 --- a/crates/egui/src/widgets/spinner.rs +++ b/crates/egui/src/widgets/spinner.rs @@ -32,6 +32,7 @@ impl Spinner { self } + /// Paint the spinner in the given rectangle. pub fn paint_at(&self, ui: &Ui, rect: Rect) { if ui.is_rect_visible(rect) { ui.ctx().request_repaint(); diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 8589faf8488..677e746392e 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -19,7 +19,7 @@ crate-type = ["cdylib", "rlib"] default = ["glow", "persistence"] http = ["ehttp", "image", "poll-promise", "egui_extras/image"] -image_viewer = ["image", "egui_extras/all-loaders", "rfd"] +image_viewer = ["image", "egui_extras/all_loaders", "rfd"] persistence = ["eframe/persistence", "egui/persistence", "serde"] web_screen_reader = ["eframe/web_screen_reader"] # experimental serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"] diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index 079446277f9..71a4611b670 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -165,7 +165,7 @@ pub struct WrapApp { impl WrapApp { pub fn new(_cc: &eframe::CreationContext<'_>) -> Self { - egui_extras::loaders::install(&_cc.egui_ctx); + egui_extras::install_image_loaders(&_cc.egui_ctx); #[allow(unused_mut)] let mut slf = Self { diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index 19cfadfcecb..9cc7d2ca6d8 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -201,11 +201,12 @@ impl WidgetGallery { ui.add(egui::Image::new(egui_icon.clone())); ui.end_row(); - ui.add(doc_link_label("ImageButton", "ImageButton")); + ui.add(doc_link_label( + "Button with image", + "Button::image_and_text", + )); if ui - .add(egui::ImageButton::new( - egui::Image::from(egui_icon).max_size(egui::Vec2::splat(16.0)), - )) + .add(egui::Button::image_and_text(egui_icon, "Click me!")) .clicked() { *boolean = !*boolean; diff --git a/crates/egui_extras/Cargo.toml b/crates/egui_extras/Cargo.toml index b6ed2245fe2..d2d21068dd1 100644 --- a/crates/egui_extras/Cargo.toml +++ b/crates/egui_extras/Cargo.toml @@ -27,7 +27,7 @@ all-features = true default = ["dep:mime_guess"] ## Shorthand for enabling the different types of image loaders (`file`, `http`, `image`, `svg`). -all-loaders = ["file", "http", "image", "svg"] +all_loaders = ["file", "http", "image", "svg"] ## Enable [`DatePickerButton`] widget. datepicker = ["chrono"] @@ -38,6 +38,14 @@ file = ["dep:mime_guess"] ## Add support for loading images via HTTP. http = ["dep:ehttp"] +## Add support for loading images with the [`image`](https://docs.rs/image) crate. +## +## You also need to ALSO opt-in to the image formats you want to support, like so: +## ```toml +## image = { version = "0.24", features = ["jpeg", "png"] } +## ``` +image = ["dep:image"] + ## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. ## ## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. @@ -71,12 +79,6 @@ chrono = { version = "0.4", optional = true, default-features = false, features ## Enable this when generating docs. document-features = { version = "0.2", optional = true } -## Add support for loading images with the [`image`](https://docs.rs/image) crate. -## -## You also need to ALSO opt-in to the image formats you want to support, like so: -## ```toml -## image = { version = "0.24", features = ["jpeg", "png"] } -## ``` image = { version = "0.24", optional = true, default-features = false } # file feature diff --git a/crates/egui_extras/src/lib.rs b/crates/egui_extras/src/lib.rs index e64486772ba..64bd808bea2 100644 --- a/crates/egui_extras/src/lib.rs +++ b/crates/egui_extras/src/lib.rs @@ -34,6 +34,8 @@ pub use crate::sizing::Size; pub use crate::strip::*; pub use crate::table::*; +pub use loaders::install_image_loaders; + // --------------------------------------------------------------------------- mod profiling_scopes { diff --git a/crates/egui_extras/src/loaders.rs b/crates/egui_extras/src/loaders.rs index 4b689672d33..0342fcabc03 100644 --- a/crates/egui_extras/src/loaders.rs +++ b/crates/egui_extras/src/loaders.rs @@ -1,22 +1,25 @@ // TODO: automatic cache eviction -/// Installs the default set of loaders. +/// Installs a set of image loaders. /// -/// - `file` loader on non-Wasm targets -/// - `http` loader (with the `http` feature) -/// - `image` loader (with the `image` feature) -/// - `svg` loader with the `svg` feature +/// Calling this enables the use of [`egui::Image`] and [`egui::Ui::image`]. +/// +/// ⚠ This will do nothing and you won't see any images unless you also enable some feature flags on `egui_extras`: +/// +/// - `file` feature: `file://` loader on non-Wasm targets +/// - `http` feature: `http(s)://` loader +/// - `image` feature: Loader of png, jpeg etc using the [`image`] crate +/// - `svg` feature: `.svg` loader /// /// Calling this multiple times on the same [`egui::Context`] is safe. /// It will never install duplicate loaders. /// -/// ⚠ This will do nothing and you won't see any images unless you enable some features: -/// -/// - If you just want to be able to load `file://` and `http://` URIs, enable the `all-loaders` feature. +/// - If you just want to be able to load `file://` and `http://` URIs, enable the `all_loaders` feature. /// - The supported set of image formats is configured by adding the [`image`](https://crates.io/crates/image) /// crate as your direct dependency, and enabling features on it: /// /// ```toml,ignore +/// egui_extras = { version = "*", features = ["all_loaders"] } /// image = { version = "0.24", features = ["jpeg", "png"] } /// ``` /// @@ -39,7 +42,8 @@ /// It will attempt to load `http://` and `https://` URIs, and infer the content type from the `Content-Type` header. /// /// The `image` loader is an [`ImageLoader`][`egui::load::ImageLoader`]. -/// It will attempt to load any URI with any extension other than `svg`. It will also load any URI without an extension. +/// It will attempt to load any URI with any extension other than `svg`. +/// It will also try to load any URI without an extension. /// The content type specified by [`BytesPoll::Ready::mime`][`egui::load::BytesPoll::Ready::mime`] always takes precedence. /// This means that even if the URI has a `png` extension, and the `png` image format is enabled, if the content type is /// not one of the supported and enabled image formats, the loader will return [`LoadError::NotSupported`][`egui::load::LoadError::NotSupported`], @@ -51,7 +55,7 @@ /// and must include `svg` for it to be considered supported. For example, `image/svg+xml` would be loaded by the `svg` loader. /// /// See [`egui::load`] for more information about how loaders work. -pub fn install(ctx: &egui::Context) { +pub fn install_image_loaders(ctx: &egui::Context) { #[cfg(all(not(target_arch = "wasm32"), feature = "file"))] if !ctx.is_loader_installed(self::file_loader::FileLoader::ID) { ctx.add_bytes_loader(std::sync::Arc::new(self::file_loader::FileLoader::default())); @@ -86,7 +90,7 @@ pub fn install(ctx: &egui::Context) { not(feature = "image"), not(feature = "svg") ))] - log::warn!("`loaders::install` was called, but no loaders are enabled"); + log::warn!("`install_image_loaders` was called, but no loaders are enabled"); let _ = ctx; } diff --git a/crates/egui_extras/src/loaders/ehttp_loader.rs b/crates/egui_extras/src/loaders/ehttp_loader.rs index 0c8f6b3bf19..8cc9a6e714e 100644 --- a/crates/egui_extras/src/loaders/ehttp_loader.rs +++ b/crates/egui_extras/src/loaders/ehttp_loader.rs @@ -72,7 +72,7 @@ impl BytesLoader for EhttpLoader { bytes: Bytes::Shared(file.bytes), mime: file.mime, }), - Poll::Ready(Err(err)) => Err(LoadError::Custom(err)), + Poll::Ready(Err(err)) => Err(LoadError::Loading(err)), Poll::Pending => Ok(BytesPoll::Pending { size: None }), } } else { diff --git a/crates/egui_extras/src/loaders/file_loader.rs b/crates/egui_extras/src/loaders/file_loader.rs index 2dd907178c0..e86cb42f37b 100644 --- a/crates/egui_extras/src/loaders/file_loader.rs +++ b/crates/egui_extras/src/loaders/file_loader.rs @@ -45,7 +45,7 @@ impl BytesLoader for FileLoader { bytes: Bytes::Shared(file.bytes), mime: file.mime, }), - Poll::Ready(Err(err)) => Err(LoadError::Custom(err)), + Poll::Ready(Err(err)) => Err(LoadError::Loading(err)), Poll::Pending => Ok(BytesPoll::Pending { size: None }), } } else { diff --git a/crates/egui_extras/src/loaders/image_loader.rs b/crates/egui_extras/src/loaders/image_loader.rs index b9374fe4078..c566e65499d 100644 --- a/crates/egui_extras/src/loaders/image_loader.rs +++ b/crates/egui_extras/src/loaders/image_loader.rs @@ -50,7 +50,7 @@ impl ImageLoader for ImageCrateLoader { if let Some(entry) = cache.get(uri).cloned() { match entry { Ok(image) => Ok(ImagePoll::Ready { image }), - Err(err) => Err(LoadError::Custom(err)), + Err(err) => Err(LoadError::Loading(err)), } } else { match ctx.try_load_bytes(uri) { @@ -68,7 +68,7 @@ impl ImageLoader for ImageCrateLoader { cache.insert(uri.into(), result.clone()); match result { Ok(image) => Ok(ImagePoll::Ready { image }), - Err(err) => Err(LoadError::Custom(err)), + Err(err) => Err(LoadError::Loading(err)), } } Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }), diff --git a/crates/egui_extras/src/loaders/svg_loader.rs b/crates/egui_extras/src/loaders/svg_loader.rs index 6af69a6142e..c5ac37b40e9 100644 --- a/crates/egui_extras/src/loaders/svg_loader.rs +++ b/crates/egui_extras/src/loaders/svg_loader.rs @@ -42,7 +42,7 @@ impl ImageLoader for SvgLoader { if let Some(entry) = cache.get(&(uri.clone(), size_hint)).cloned() { match entry { Ok(image) => Ok(ImagePoll::Ready { image }), - Err(err) => Err(LoadError::Custom(err)), + Err(err) => Err(LoadError::Loading(err)), } } else { match ctx.try_load_bytes(&uri) { @@ -60,7 +60,7 @@ impl ImageLoader for SvgLoader { cache.insert((uri, size_hint), result.clone()); match result { Ok(image) => Ok(ImagePoll::Ready { image }), - Err(err) => Err(LoadError::Custom(err)), + Err(err) => Err(LoadError::Loading(err)), } } Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }), diff --git a/crates/egui_glow/src/painter.rs b/crates/egui_glow/src/painter.rs index 64a8be0be6a..c04dd927e9b 100644 --- a/crates/egui_glow/src/painter.rs +++ b/crates/egui_glow/src/painter.rs @@ -370,25 +370,6 @@ impl Painter { Primitive::Callback(callback) => { if callback.rect.is_positive() { crate::profile_scope!("callback"); - // Transform callback rect to physical pixels: - let rect_min_x = pixels_per_point * callback.rect.min.x; - let rect_min_y = pixels_per_point * callback.rect.min.y; - let rect_max_x = pixels_per_point * callback.rect.max.x; - let rect_max_y = pixels_per_point * callback.rect.max.y; - - let rect_min_x = rect_min_x.round() as i32; - let rect_min_y = rect_min_y.round() as i32; - let rect_max_x = rect_max_x.round() as i32; - let rect_max_y = rect_max_y.round() as i32; - - unsafe { - self.gl.viewport( - rect_min_x, - size_in_pixels.1 as i32 - rect_max_y, - rect_max_x - rect_min_x, - rect_max_y - rect_min_y, - ); - } let info = egui::PaintCallbackInfo { viewport: callback.rect, @@ -397,6 +378,16 @@ impl Painter { screen_size_px, }; + let viewport_px = info.viewport_in_pixels(); + unsafe { + self.gl.viewport( + viewport_px.left_px.round() as _, + viewport_px.from_bottom_px.round() as _, + viewport_px.width_px.round() as _, + viewport_px.height_px.round() as _, + ); + } + if let Some(callback) = callback.callback.downcast_ref::() { (callback.f)(info, self); } else { diff --git a/crates/epaint/src/shape.rs b/crates/epaint/src/shape.rs index 8854a2e860e..718da636bff 100644 --- a/crates/epaint/src/shape.rs +++ b/crates/epaint/src/shape.rs @@ -787,6 +787,8 @@ pub struct PaintCallbackInfo { /// Rect is the [-1, +1] of the Normalized Device Coordinates. /// /// Note than only a portion of this may be visible due to [`Self::clip_rect`]. + /// + /// This comes from [`PaintCallback::rect`]. pub viewport: Rect, /// Clip rectangle in points. @@ -819,7 +821,7 @@ pub struct ViewportInPixels { } impl PaintCallbackInfo { - fn points_to_pixels(&self, rect: &Rect) -> ViewportInPixels { + fn pixels_from_points(&self, rect: &Rect) -> ViewportInPixels { ViewportInPixels { left_px: rect.min.x * self.pixels_per_point, top_px: rect.min.y * self.pixels_per_point, @@ -831,12 +833,12 @@ impl PaintCallbackInfo { /// The viewport rectangle. This is what you would use in e.g. `glViewport`. pub fn viewport_in_pixels(&self) -> ViewportInPixels { - self.points_to_pixels(&self.viewport) + self.pixels_from_points(&self.viewport) } /// The "scissor" or "clip" rectangle. This is what you would use in e.g. `glScissor`. pub fn clip_rect_in_pixels(&self) -> ViewportInPixels { - self.points_to_pixels(&self.clip_rect) + self.pixels_from_points(&self.clip_rect) } } @@ -846,6 +848,8 @@ impl PaintCallbackInfo { #[derive(Clone)] pub struct PaintCallback { /// Where to paint. + /// + /// This will become [`PaintCallbackInfo::viewport`]. pub rect: Rect, /// Paint something custom (e.g. 3D stuff). diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 44300cab286..49cbd31198f 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1063,7 +1063,7 @@ fn mul_color(color: Color32, factor: f32) -> Color32 { /// /// For performance reasons it is smart to reuse the same [`Tessellator`]. /// -/// Se also [`tessellate_shapes`], a convenient wrapper around [`Tessellator`]. +/// See also [`tessellate_shapes`], a convenient wrapper around [`Tessellator`]. pub struct Tessellator { pixels_per_point: f32, options: TessellationOptions, diff --git a/examples/images/Cargo.toml b/examples/images/Cargo.toml index e6fc82e3909..851fa8f79ff 100644 --- a/examples/images/Cargo.toml +++ b/examples/images/Cargo.toml @@ -12,7 +12,7 @@ publish = false eframe = { path = "../../crates/eframe", features = [ "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO ] } -egui_extras = { path = "../../crates/egui_extras", features = ["all-loaders"] } +egui_extras = { path = "../../crates/egui_extras", features = ["all_loaders"] } env_logger = "0.10" image = { version = "0.24", default-features = false, features = [ "jpeg", diff --git a/examples/images/src/main.rs b/examples/images/src/main.rs index 656456cbb60..87a680d1e5a 100644 --- a/examples/images/src/main.rs +++ b/examples/images/src/main.rs @@ -1,12 +1,11 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release use eframe::egui; -use eframe::epaint::vec2; fn main() -> Result<(), eframe::Error> { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). let options = eframe::NativeOptions { - drag_and_drop_support: true, + initial_window_size: Some(egui::vec2(600.0, 800.0)), ..Default::default() }; eframe::run_native( @@ -14,7 +13,7 @@ fn main() -> Result<(), eframe::Error> { options, Box::new(|cc| { // The following call is needed to load images when using `ui.image` and `egui::Image`: - egui_extras::loaders::install(&cc.egui_ctx); + egui_extras::install_image_loaders(&cc.egui_ctx); Box::::default() }), ) @@ -27,10 +26,8 @@ impl eframe::App for MyApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { egui::CentralPanel::default().show(ctx, |ui| { egui::ScrollArea::new([true, true]).show(ui, |ui| { - ui.add( - egui::Image::new(egui::include_image!("ferris.svg")) - .fit_to_fraction(vec2(1.0, 0.5)), - ); + ui.image(egui::include_image!("ferris.svg")); + ui.add( egui::Image::new("https://picsum.photos/seed/1.759706314/1024") .rounding(egui::Rounding::same(10.0)),