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
This commit is contained in:
Emil Ernerfeldt 2023-09-14 16:33:10 +02:00 committed by GitHub
parent e367c20779
commit d7d222d3f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 406 additions and 279 deletions

View File

@ -445,6 +445,13 @@ impl Renderer {
needs_reset = true; 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 // 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 // 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 // viewport during the paint callback, effectively overriding this
// one. // one.
let min = (callback.rect.min.to_vec2() * pixels_per_point).round(); let viewport_px = info.viewport_in_pixels();
let max = (callback.rect.max.to_vec2() * pixels_per_point).round();
render_pass.set_viewport( render_pass.set_viewport(
min.x, viewport_px.left_px,
min.y, viewport_px.top_px,
max.x - min.x, viewport_px.width_px,
max.y - min.y, viewport_px.height_px,
0.0, 0.0,
1.0, 1.0,
); );
} }
cbfn.0.paint( cbfn.0.paint(info, render_pass, &self.callback_resources);
PaintCallbackInfo {
viewport: callback.rect,
clip_rect: *clip_rect,
pixels_per_point,
screen_size_px: size_in_pixels,
},
render_pass,
&self.callback_resources,
);
} }
} }
} }

View File

@ -1121,9 +1121,16 @@ impl Context {
/// Allocate a texture. /// Allocate a texture.
/// ///
/// In order to display an image you must convert it to a texture using this function. /// This is for advanced users.
/// Most users should use [`crate::Ui::image`] or [`Self::try_load_texture`]
/// instead.
/// ///
/// Make sure to only call this once for each image, i.e. NOT in your main GUI code. /// 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.
/// 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`]. /// 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( pub fn load_texture(
&self, &self,
name: impl Into<String>, name: impl Into<String>,
@ -1912,6 +1919,9 @@ impl Context {
/// Associate some static bytes with a `uri`. /// Associate some static bytes with a `uri`.
/// ///
/// The same `uri` may be passed to [`Ui::image`] later to load the bytes as an image. /// 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<Cow<'static, str>>, bytes: impl Into<Bytes>) { pub fn include_bytes(&self, uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) {
self.loaders().include.insert(uri, bytes); self.loaders().include.insert(uri, bytes);
} }
@ -1921,32 +1931,32 @@ impl Context {
pub fn is_loader_installed(&self, id: &str) -> bool { pub fn is_loader_installed(&self, id: &str) -> bool {
let loaders = self.loaders(); let loaders = self.loaders();
let in_bytes = loaders.bytes.lock().iter().any(|loader| loader.id() == id); loaders.bytes.lock().iter().any(|l| l.id() == id)
let in_image = loaders.image.lock().iter().any(|loader| loader.id() == id); || loaders.image.lock().iter().any(|l| l.id() == id)
let in_texture = loaders || loaders.texture.lock().iter().any(|l| l.id() == id)
.texture
.lock()
.iter()
.any(|loader| loader.id() == id);
in_bytes || in_image || in_texture
} }
/// 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. /// See [`load`] for more information.
pub fn add_bytes_loader(&self, loader: Arc<dyn load::BytesLoader + Send + Sync + 'static>) { pub fn add_bytes_loader(&self, loader: Arc<dyn load::BytesLoader + Send + Sync + 'static>) {
self.loaders().bytes.lock().push(loader); 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. /// See [`load`] for more information.
pub fn add_image_loader(&self, loader: Arc<dyn load::ImageLoader + Send + Sync + 'static>) { pub fn add_image_loader(&self, loader: Arc<dyn load::ImageLoader + Send + Sync + 'static>) {
self.loaders().image.lock().push(loader); 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. /// See [`load`] for more information.
pub fn add_texture_loader(&self, loader: Arc<dyn load::TextureLoader + Send + Sync + 'static>) { pub fn add_texture_loader(&self, loader: Arc<dyn load::TextureLoader + Send + Sync + 'static>) {
@ -2009,23 +2019,27 @@ impl Context {
/// # Errors /// # Errors
/// This may fail with: /// This may fail with:
/// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`. /// - [`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`! /// ⚠ May deadlock if called from within a `BytesLoader`!
/// ///
/// [not_supported]: crate::load::LoadError::NotSupported /// [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 { pub fn try_load_bytes(&self, uri: &str) -> load::BytesLoadResult {
crate::profile_function!(); 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) { match loader.load(self, uri) {
Err(load::LoadError::NotSupported) => continue, Err(load::LoadError::NotSupported) => continue,
result => return result, result => return result,
} }
} }
Err(load::LoadError::NotSupported) Err(load::LoadError::NoMatchingBytesLoader)
} }
/// Try loading the image from the given uri using any available image loaders. /// Try loading the image from the given uri using any available image loaders.
@ -2041,30 +2055,31 @@ impl Context {
/// This may fail with: /// This may fail with:
/// - [`LoadError::NoImageLoaders`][no_image_loaders] if tbere are no registered image loaders. /// - [`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::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`! /// ⚠ May deadlock if called from within an `ImageLoader`!
/// ///
/// [no_image_loaders]: crate::load::LoadError::NoImageLoaders /// [no_image_loaders]: crate::load::LoadError::NoImageLoaders
/// [not_supported]: crate::load::LoadError::NotSupported /// [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 { pub fn try_load_image(&self, uri: &str, size_hint: load::SizeHint) -> load::ImageLoadResult {
crate::profile_function!(); crate::profile_function!();
let loaders = self.loaders(); let loaders = self.loaders();
let loaders = loaders.image.lock(); let image_loaders = loaders.image.lock();
if loaders.is_empty() { if image_loaders.is_empty() {
return Err(load::LoadError::NoImageLoaders); 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) { match loader.load(self, uri, size_hint) {
Err(load::LoadError::NotSupported) => continue, Err(load::LoadError::NotSupported) => continue,
result => return result, result => return result,
} }
} }
Err(load::LoadError::NotSupported) Err(load::LoadError::NoMatchingImageLoader)
} }
/// Try loading the texture from the given uri using any available texture loaders. /// Try loading the texture from the given uri using any available texture loaders.
@ -2079,12 +2094,12 @@ impl Context {
/// # Errors /// # Errors
/// This may fail with: /// This may fail with:
/// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`. /// - [`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`! /// ⚠ May deadlock if called from within a `TextureLoader`!
/// ///
/// [not_supported]: crate::load::LoadError::NotSupported /// [not_supported]: crate::load::LoadError::NotSupported
/// [custom]: crate::load::LoadError::Custom /// [custom]: crate::load::LoadError::Loading
pub fn try_load_texture( pub fn try_load_texture(
&self, &self,
uri: &str, uri: &str,
@ -2093,17 +2108,22 @@ impl Context {
) -> load::TextureLoadResult { ) -> load::TextureLoadResult {
crate::profile_function!(); 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) { match loader.load(self, uri, texture_options, size_hint) {
Err(load::LoadError::NotSupported) => continue, Err(load::LoadError::NotSupported) => continue,
result => return result, result => return result,
} }
} }
Err(load::LoadError::NotSupported) Err(load::LoadError::NoMatchingTextureLoader)
} }
fn loaders(&self) -> Arc<Loaders> { /// The loaders of bytes, images, and textures.
pub fn loaders(&self) -> Arc<Loaders> {
crate::profile_function!(); crate::profile_function!();
self.read(|this| this.loaders.clone()) self.read(|this| this.loaders.clone())
} }

View File

@ -437,13 +437,16 @@ pub fn warn_if_debug_build(ui: &mut crate::Ui) {
/// egui::Image::new(egui::include_image!("../assets/ferris.png")) /// egui::Image::new(egui::include_image!("../assets/ferris.png"))
/// .rounding(egui::Rounding::same(6.0)) /// .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_export]
macro_rules! include_image { macro_rules! include_image {
($path: literal) => { ($path: literal) => {
$crate::ImageSource::Bytes( $crate::ImageSource::Bytes(
::std::borrow::Cow::Borrowed($path), ::std::borrow::Cow::Borrowed(concat!("bytes://", $path)), // uri
$crate::load::Bytes::Static(include_bytes!($path)), $crate::load::Bytes::Static(include_bytes!($path)),
) )
}; };

View File

@ -3,8 +3,8 @@
//! If you just want to display some images, [`egui_extras`](https://crates.io/crates/egui_extras/) //! 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. //! 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. //! 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) //! 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. //! in your app's setup code.
//! 3. Use [`Ui::image`][`crate::ui::Ui::image`] with some [`ImageSource`][`crate::ImageSource`]. //! 3. Use [`Ui::image`][`crate::ui::Ui::image`] with some [`ImageSource`][`crate::ImageSource`].
//! //!
@ -55,8 +55,6 @@
mod bytes_loader; mod bytes_loader;
mod texture_loader; mod texture_loader;
use self::bytes_loader::DefaultBytesLoader;
use self::texture_loader::DefaultTextureLoader;
use crate::Context; use crate::Context;
use ahash::HashMap; use ahash::HashMap;
use epaint::mutex::Mutex; use epaint::mutex::Mutex;
@ -69,27 +67,50 @@ use std::fmt::Debug;
use std::ops::Deref; use std::ops::Deref;
use std::{error::Error as StdError, fmt::Display, sync::Arc}; 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. /// Represents a failed attempt at loading an image.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum LoadError { pub enum LoadError {
/// There are no image loaders installed. /// Programmer error: There are no image loaders installed.
NoImageLoaders, NoImageLoaders,
/// This loader does not support this protocol or image format. /// A specific loader does not support this schema, protocol or image format.
NotSupported, NotSupported,
/// A custom error message (e.g. "File not found: foo.png"). /// Programmer error: Failed to find the bytes for this image because
Custom(String), /// 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 { impl Display for LoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { 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 \ "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"), 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<BytesPoll>; pub type BytesLoadResult = Result<BytesPoll>;
/// 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 /// It should also provide any subsequent loaders a hint for what the bytes may
/// represent using [`BytesPoll::Ready::mime`], if it can be inferred. /// represent using [`BytesPoll::Ready::mime`], if it can be inferred.
@ -261,7 +283,7 @@ pub trait BytesLoader {
/// # Errors /// # Errors
/// This may fail with: /// This may fail with:
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`. /// - [`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; fn load(&self, ctx: &Context, uri: &str) -> BytesLoadResult;
/// Forget the given `uri`. /// Forget the given `uri`.
@ -305,7 +327,7 @@ pub enum ImagePoll {
pub type ImageLoadResult = Result<ImagePoll>; pub type ImageLoadResult = Result<ImagePoll>;
/// 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`. /// Implementations are expected to cache at least each `URI`.
pub trait ImageLoader { pub trait ImageLoader {
@ -328,7 +350,7 @@ pub trait ImageLoader {
/// # Errors /// # Errors
/// This may fail with: /// This may fail with:
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`. /// - [`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; fn load(&self, ctx: &Context, uri: &str, size_hint: SizeHint) -> ImageLoadResult;
/// Forget the given `uri`. /// Forget the given `uri`.
@ -420,7 +442,14 @@ impl TexturePoll {
pub type TextureLoadResult = Result<TexturePoll>; pub type TextureLoadResult = Result<TexturePoll>;
/// 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)`. /// Implementations are expected to cache each combination of `(URI, TextureOptions)`.
pub trait TextureLoader { pub trait TextureLoader {
@ -443,7 +472,7 @@ pub trait TextureLoader {
/// # Errors /// # Errors
/// This may fail with: /// This may fail with:
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`. /// - [`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( fn load(
&self, &self,
ctx: &Context, ctx: &Context,
@ -479,7 +508,8 @@ type ImageLoaderImpl = Arc<dyn ImageLoader + Send + Sync + 'static>;
type TextureLoaderImpl = Arc<dyn TextureLoader + Send + Sync + 'static>; type TextureLoaderImpl = Arc<dyn TextureLoader + Send + Sync + 'static>;
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct Loaders { /// The loaders of bytes, images, and textures.
pub struct Loaders {
pub include: Arc<DefaultBytesLoader>, pub include: Arc<DefaultBytesLoader>,
pub bytes: Mutex<Vec<BytesLoaderImpl>>, pub bytes: Mutex<Vec<BytesLoaderImpl>>,
pub image: Mutex<Vec<ImageLoaderImpl>>, pub image: Mutex<Vec<ImageLoaderImpl>>,

View File

@ -1,5 +1,8 @@
use super::*; 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)] #[derive(Default)]
pub struct DefaultBytesLoader { pub struct DefaultBytesLoader {
cache: Mutex<HashMap<Cow<'static, str>, Bytes>>, cache: Mutex<HashMap<Cow<'static, str>, Bytes>>,
@ -27,13 +30,22 @@ impl BytesLoader for DefaultBytesLoader {
} }
fn load(&self, _: &Context, uri: &str) -> BytesLoadResult { 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() { match self.cache.lock().get(uri).cloned() {
Some(bytes) => Ok(BytesPoll::Ready { Some(bytes) => Ok(BytesPoll::Ready {
size: None, size: None,
bytes, bytes,
mime: None, 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)
}
}
} }
} }

View File

@ -207,9 +207,6 @@ pub struct Style {
/// ///
/// This only affects a few egui widgets. /// This only affects a few egui widgets.
pub explanation_tooltips: bool, pub explanation_tooltips: bool,
/// Show a spinner when loading an image.
pub image_loading_spinners: bool,
} }
impl Style { impl Style {
@ -556,6 +553,9 @@ pub struct Visuals {
/// all turn your cursor into [`CursorIcon::PointingHand`] when a button is /// all turn your cursor into [`CursorIcon::PointingHand`] when a button is
/// hovered) but it is inconsistent with native UI toolkits. /// hovered) but it is inconsistent with native UI toolkits.
pub interact_cursor: Option<CursorIcon>, pub interact_cursor: Option<CursorIcon>,
/// Show a spinner when loading an image.
pub image_loading_spinners: bool,
} }
impl Visuals { impl Visuals {
@ -741,7 +741,6 @@ impl Default for Style {
animation_time: 1.0 / 12.0, animation_time: 1.0 / 12.0,
debug: Default::default(), debug: Default::default(),
explanation_tooltips: false, explanation_tooltips: false,
image_loading_spinners: true,
} }
} }
} }
@ -821,6 +820,8 @@ impl Visuals {
slider_trailing_fill: false, slider_trailing_fill: false,
interact_cursor: None, interact_cursor: None,
image_loading_spinners: true,
} }
} }
@ -994,7 +995,6 @@ impl Style {
animation_time, animation_time,
debug, debug,
explanation_tooltips, explanation_tooltips,
image_loading_spinners,
} = self; } = self;
visuals.light_dark_radio_buttons(ui); visuals.light_dark_radio_buttons(ui);
@ -1062,9 +1062,6 @@ impl Style {
"Show explanatory text when hovering DragValue:s and other egui widgets", "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)); ui.vertical_centered(|ui| reset_button(ui, self));
} }
} }
@ -1396,6 +1393,8 @@ impl Visuals {
slider_trailing_fill, slider_trailing_fill,
interact_cursor, interact_cursor,
image_loading_spinners,
} = self; } = self;
ui.collapsing("Background Colors", |ui| { 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)); ui.vertical_centered(|ui| reset_button(ui, self));
} }
} }

View File

@ -1561,7 +1561,7 @@ impl Ui {
/// Show an image available at the given `uri`. /// Show an image available at the given `uri`.
/// ///
/// ⚠ This will do nothing unless you install some image loaders first! /// ⚠ 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. /// 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 /// Using [`include_image`] is often the most ergonomic, and the path
/// from a file with a statically known path, unless you really want to /// will be resolved at compile-time and embedded in the binary.
/// load it at runtime instead! /// 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`]. /// See also [`crate::Image`], [`crate::ImageSource`].
#[inline] #[inline]
@ -1687,7 +1688,7 @@ impl Ui {
/// # }); /// # });
/// ``` /// ```
/// ///
/// Se also [`Self::scope`]. /// See also [`Self::scope`].
pub fn group<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> { pub fn group<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
crate::Frame::group(self.style()).show(self, add_contents) crate::Frame::group(self.style()).show(self, add_contents)
} }

View File

@ -1,5 +1,3 @@
use crate::load::SizedTexture;
use crate::load::TexturePoll;
use crate::*; use crate::*;
/// Clickable button with text. /// Clickable button with text.
@ -22,7 +20,8 @@ use crate::*;
/// ``` /// ```
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] #[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct Button<'a> { pub struct Button<'a> {
text: WidgetText, image: Option<Image<'a>>,
text: Option<WidgetText>,
shortcut_text: WidgetText, shortcut_text: WidgetText,
wrap: Option<bool>, wrap: Option<bool>,
@ -34,13 +33,30 @@ pub struct Button<'a> {
frame: Option<bool>, frame: Option<bool>,
min_size: Vec2, min_size: Vec2,
rounding: Option<Rounding>, rounding: Option<Rounding>,
image: Option<Image<'a>>, selected: bool,
} }
impl<'a> Button<'a> { impl<'a> Button<'a> {
pub fn new(text: impl Into<WidgetText>) -> Self { pub fn new(text: impl Into<WidgetText>) -> 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<Image<'a>>) -> 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<Image<'a>>, text: impl Into<WidgetText>) -> Self {
Self::opt_image_and_text(Some(image.into()), Some(text.into()))
}
pub fn opt_image_and_text(image: Option<Image<'a>>, text: Option<WidgetText>) -> Self {
Self { Self {
text: text.into(), text,
image,
shortcut_text: Default::default(), shortcut_text: Default::default(),
wrap: None, wrap: None,
fill: None, fill: None,
@ -50,16 +66,7 @@ impl<'a> Button<'a> {
frame: None, frame: None,
min_size: Vec2::ZERO, min_size: Vec2::ZERO,
rounding: None, rounding: None,
image: None, selected: false,
}
}
/// 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<Image<'a>>, text: impl Into<WidgetText>) -> Self {
Self {
image: Some(image.into()),
..Self::new(text)
} }
} }
@ -94,7 +101,9 @@ impl<'a> Button<'a> {
/// Make this a small button, suitable for embedding into text. /// Make this a small button, suitable for embedding into text.
pub fn small(mut self) -> Self { 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.small = true;
self self
} }
@ -133,12 +142,19 @@ impl<'a> Button<'a> {
self.shortcut_text = shortcut_text.into(); self.shortcut_text = shortcut_text.into();
self self
} }
/// If `true`, mark this button as "selected".
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
} }
impl Widget for Button<'_> { impl Widget for Button<'_> {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let Button { let Button {
text, text,
image,
shortcut_text, shortcut_text,
wrap, wrap,
fill, fill,
@ -148,21 +164,35 @@ impl Widget for Button<'_> {
frame, frame,
min_size, min_size,
rounding, rounding,
image, selected,
} = self; } = 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 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 { if small {
button_padding.y = 0.0; 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; let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x;
if image.is_some() { if image.is_some() {
text_wrap_width -= image_size.x + ui.spacing().icon_spacing; 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). 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()) let shortcut_text = (!shortcut_text.is_empty())
.then(|| shortcut_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button)); .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() { 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); 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 { if let Some(shortcut_text) = &shortcut_text {
desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x; desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x;
desired_size.y = desired_size.y.max(shortcut_text.size().y); desired_size.y = desired_size.y.max(shortcut_text.size().y);
@ -191,31 +228,81 @@ impl Widget for Button<'_> {
desired_size = desired_size.at_least(min_size); desired_size = desired_size.at_least(min_size);
let (rect, mut 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())); 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) { if ui.is_rect_visible(rect) {
let visuals = ui.style().interact(&response); let visuals = ui.style().interact(&response);
if frame { let (frame_expansion, frame_rounding, frame_fill, frame_stroke) = if selected {
let fill = fill.unwrap_or(visuals.weak_bg_fill); let selection = ui.visuals().selection;
let stroke = stroke.unwrap_or(visuals.bg_stroke); (
let rounding = rounding.unwrap_or(visuals.rounding); Vec2::ZERO,
ui.painter() Rounding::ZERO,
.rect(rect.expand(visuals.expansion), rounding, fill, stroke); selection.bg_fill,
} selection.stroke,
)
let text_pos = if image.is_some() { } else if frame {
let icon_spacing = ui.spacing().icon_spacing; let expansion = Vec2::splat(visuals.expansion);
pos2( (
rect.min.x + button_padding.x + image_size.x + icon_spacing, expansion,
rect.center().y - 0.5 * text.size().y, visuals.rounding,
visuals.weak_bg_fill,
visuals.bg_stroke,
) )
} else { } else {
ui.layout() Default::default()
.align_size_within_rect(text.size(), rect.shrink2(button_padding))
.min
}; };
text.paint_with_visuals(ui.painter(), text_pos, visuals); 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(cursor_x, rect.center().y - 0.5 - (image_size.y / 2.0)),
image_size,
);
cursor_x += image_size.x;
let tlr = image.load(ui);
widgets::image::paint_texture_load_result(
ui,
&tlr,
image_rect,
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 { if let Some(shortcut_text) = shortcut_text {
let shortcut_text_pos = pos2( let shortcut_text_pos = pos2(
@ -228,29 +315,6 @@ impl Widget for Button<'_> {
ui.visuals().weak_text_color(), ui.visuals().weak_text_color(),
); );
} }
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),
),
image_size,
);
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);
}
} }
if let Some(cursor) = ui.visuals().interact_cursor { if let Some(cursor) = ui.visuals().interact_cursor {
@ -530,8 +594,10 @@ impl<'a> ImageButton<'a> {
self.sense = sense; self.sense = sense;
self 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 { let padding = if self.frame {
// so we can see that it is a button: // so we can see that it is a button:
Vec2::splat(ui.spacing().button_padding.x) Vec2::splat(ui.spacing().button_padding.x)
@ -539,7 +605,13 @@ impl<'a> ImageButton<'a> {
Vec2::ZERO 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); let (rect, response) = ui.allocate_exact_size(padded_size, self.sense);
response.widget_info(|| WidgetInfo::new(WidgetType::ImageButton)); response.widget_info(|| WidgetInfo::new(WidgetType::ImageButton));
@ -571,36 +643,19 @@ impl<'a> ImageButton<'a> {
let image_rect = ui let image_rect = ui
.layout() .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_rect = image_rect.expand2(expansion); // can make it blurry, so let's not
let image_options = ImageOptions { let image_options = ImageOptions {
rounding, // apply rounding to the image rounding, // apply rounding to the image
..self.image.image_options().clone() ..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: // Draw frame outline:
ui.painter() ui.painter()
.rect_stroke(rect.expand2(expansion), rounding, stroke); .rect_stroke(rect.expand2(expansion), rounding, stroke);
} }
response widgets::image::texture_load_result_response(self.image.source(), &tlr, 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()),
}
} }
} }

View File

@ -74,6 +74,8 @@ impl<'a> Image<'a> {
/// Load the image from some raw bytes. /// Load the image from some raw bytes.
/// ///
/// For better error messages, use the `bytes://` prefix for the URI.
///
/// See [`ImageSource::Bytes`]. /// See [`ImageSource::Bytes`].
pub fn from_bytes(uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) -> Self { pub fn from_bytes(uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) -> Self {
Self::new(ImageSource::Bytes(uri.into(), bytes.into())) Self::new(ImageSource::Bytes(uri.into(), bytes.into()))
@ -220,7 +222,7 @@ impl<'a> Image<'a> {
/// Show a spinner when the image is loading. /// 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] #[inline]
pub fn show_loading_spinner(mut self, show: bool) -> Self { pub fn show_loading_spinner(mut self, show: bool) -> Self {
self.show_loading_spinner = Some(show); self.show_loading_spinner = Some(show);
@ -237,7 +239,8 @@ impl<'a, T: Into<ImageSource<'a>>> From<T> for Image<'a> {
impl<'a> Image<'a> { impl<'a> Image<'a> {
/// Returns the size the image will occupy in the final UI. /// Returns the size the image will occupy in the final UI.
#[inline] #[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>) -> 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) self.size.get(available_size, image_size)
} }
@ -264,18 +267,9 @@ impl<'a> Image<'a> {
&self.source &self.source
} }
/// Get the `uri` that this image was constructed from.
///
/// This will return `<unknown>` for [`ImageSource::Texture`].
#[inline]
pub fn uri(&self) -> &str {
self.source.uri().unwrap_or("<unknown>")
}
/// Load the image from its [`Image::source`], returning the resulting [`SizedTexture`]. /// Load the image from its [`Image::source`], returning the resulting [`SizedTexture`].
/// ///
/// # Errors /// # Errors
///
/// May fail if they underlying [`Context::try_load_texture`] call fails. /// May fail if they underlying [`Context::try_load_texture`] call fails.
pub fn load(&self, ui: &Ui) -> TextureLoadResult { pub fn load(&self, ui: &Ui) -> TextureLoadResult {
let size_hint = self.size.hint(ui.available_size()); 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) .load(ui.ctx(), self.texture_options, size_hint)
} }
/// Paint the image in the given rectangle.
#[inline] #[inline]
pub fn paint_at(&self, ui: &mut Ui, rect: Rect, texture: &SizedTexture) { pub fn paint_at(&self, ui: &mut Ui, rect: Rect) {
paint_image_at(ui, rect, &self.image_options, texture); paint_texture_load_result(
ui,
&self.load(ui),
rect,
self.show_loading_spinner,
&self.image_options,
);
} }
} }
impl<'a> Widget for Image<'a> { impl<'a> Widget for Image<'a> {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
match self.load(ui) { let tlr = self.load(ui);
Ok(texture_poll) => { let texture_size = tlr.as_ref().ok().and_then(|t| t.size());
let texture_size = texture_poll.size(); let ui_size = self.calculate_size(ui.available_size(), texture_size);
let texture_size =
texture_size.unwrap_or_else(|| Vec2::splat(ui.style().spacing.interact_size.y)); let (rect, response) = ui.allocate_exact_size(ui_size, self.sense);
let ui_size = self.calculate_size(ui.available_size(), texture_size); paint_texture_load_result(
let (rect, response) = ui.allocate_exact_size(ui_size, self.sense); ui,
match texture_poll { &tlr,
TexturePoll::Ready { texture } => { rect,
self.paint_at(ui, rect, &texture); self.show_loading_spinner,
response &self.image_options,
} );
TexturePoll::Pending { .. } => { texture_load_result_response(&self.source, &tlr, response)
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()),
}
} }
} }
@ -353,12 +340,16 @@ pub struct ImageSize {
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ImageFit { pub enum ImageFit {
/// Fit the image to its original size, scaled by some factor. /// Fit the image to its original size, scaled by some factor.
///
/// Ignores how much space is actually available in the ui.
Original { scale: f32 }, Original { scale: f32 },
/// Fit the image to a fraction of the available size. /// Fit the image to a fraction of the available size.
Fraction(Vec2), Fraction(Vec2),
/// Fit the image to an exact size. /// Fit the image to an exact size.
///
/// Ignores how much space is actually available in the ui.
Exact(Vec2), Exact(Vec2),
} }
@ -373,7 +364,7 @@ impl ImageFit {
} }
impl ImageSize { impl ImageSize {
fn hint(&self, available_size: Vec2) -> SizeHint { pub fn hint(&self, available_size: Vec2) -> SizeHint {
if self.maintain_aspect_ratio { if self.maintain_aspect_ratio {
return SizeHint::Scale(1.0.ord()); 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 { let Self {
maintain_aspect_ratio, maintain_aspect_ratio,
max_size, max_size,
@ -449,7 +440,7 @@ impl Default for ImageSize {
/// This type tells the [`Ui`] how to load an image. /// This type tells the [`Ui`] how to load an image.
/// ///
/// This is used by [`Image::new`] and [`Ui::image`]. /// This is used by [`Image::new`] and [`Ui::image`].
#[derive(Debug, Clone)] #[derive(Clone)]
pub enum ImageSource<'a> { pub enum ImageSource<'a> {
/// Load the image from a URI. /// Load the image from a URI.
/// ///
@ -468,6 +459,8 @@ pub enum ImageSource<'a> {
/// Load the image from some raw bytes. /// Load the image from some raw bytes.
/// ///
/// For better error messages, use the `bytes://` prefix for the URI.
///
/// The [`Bytes`] may be: /// The [`Bytes`] may be:
/// - `'static`, obtained from `include_bytes!` or similar /// - `'static`, obtained from `include_bytes!` or similar
/// - Anything that can be converted to `Arc<[u8]>` /// - Anything that can be converted to `Arc<[u8]>`
@ -480,6 +473,15 @@ pub enum ImageSource<'a> {
Bytes(Cow<'static, str>, Bytes), 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> { impl<'a> ImageSource<'a> {
/// # Errors /// # Errors
/// Failure to load the texture. /// Failure to load the texture.
@ -514,7 +516,7 @@ pub fn paint_texture_load_result(
ui: &Ui, ui: &Ui,
tlr: &TextureLoadResult, tlr: &TextureLoadResult,
rect: Rect, rect: Rect,
show_loading_spinner: bool, show_loading_spinner: Option<bool>,
options: &ImageOptions, options: &ImageOptions,
) { ) {
match tlr { match tlr {
@ -522,6 +524,8 @@ pub fn paint_texture_load_result(
paint_image_at(ui, rect, options, texture); paint_image_at(ui, rect, options, texture);
} }
Ok(TexturePoll::Pending { .. }) => { Ok(TexturePoll::Pending { .. }) => {
let show_loading_spinner =
show_loading_spinner.unwrap_or(ui.visuals().image_loading_spinners);
if show_loading_spinner { if show_loading_spinner {
Spinner::new().paint_at(ui, rect); 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( pub fn texture_load_result_response(
source: &ImageSource<'_>, source: &ImageSource<'_>,
tlr: &TextureLoadResult, tlr: &TextureLoadResult,
@ -547,13 +552,13 @@ pub fn texture_load_result_response(
match tlr { match tlr {
Ok(TexturePoll::Ready { .. }) => response, Ok(TexturePoll::Ready { .. }) => response,
Ok(TexturePoll::Pending { .. }) => { Ok(TexturePoll::Pending { .. }) => {
if let Some(uri) = source.uri() { let uri = source.uri().unwrap_or("image");
response.on_hover_text(format!("Loading {uri}")) response.on_hover_text(format!("Loading {uri}"))
} else { }
response.on_hover_text("Loading image…") 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()),
} }
} }

View File

@ -32,6 +32,7 @@ impl Spinner {
self self
} }
/// Paint the spinner in the given rectangle.
pub fn paint_at(&self, ui: &Ui, rect: Rect) { pub fn paint_at(&self, ui: &Ui, rect: Rect) {
if ui.is_rect_visible(rect) { if ui.is_rect_visible(rect) {
ui.ctx().request_repaint(); ui.ctx().request_repaint();

View File

@ -19,7 +19,7 @@ crate-type = ["cdylib", "rlib"]
default = ["glow", "persistence"] default = ["glow", "persistence"]
http = ["ehttp", "image", "poll-promise", "egui_extras/image"] 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"] persistence = ["eframe/persistence", "egui/persistence", "serde"]
web_screen_reader = ["eframe/web_screen_reader"] # experimental web_screen_reader = ["eframe/web_screen_reader"] # experimental
serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"] serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]

View File

@ -165,7 +165,7 @@ pub struct WrapApp {
impl WrapApp { impl WrapApp {
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self { 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)] #[allow(unused_mut)]
let mut slf = Self { let mut slf = Self {

View File

@ -201,11 +201,12 @@ impl WidgetGallery {
ui.add(egui::Image::new(egui_icon.clone())); ui.add(egui::Image::new(egui_icon.clone()));
ui.end_row(); ui.end_row();
ui.add(doc_link_label("ImageButton", "ImageButton")); ui.add(doc_link_label(
"Button with image",
"Button::image_and_text",
));
if ui if ui
.add(egui::ImageButton::new( .add(egui::Button::image_and_text(egui_icon, "Click me!"))
egui::Image::from(egui_icon).max_size(egui::Vec2::splat(16.0)),
))
.clicked() .clicked()
{ {
*boolean = !*boolean; *boolean = !*boolean;

View File

@ -27,7 +27,7 @@ all-features = true
default = ["dep:mime_guess"] default = ["dep:mime_guess"]
## Shorthand for enabling the different types of image loaders (`file`, `http`, `image`, `svg`). ## 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. ## Enable [`DatePickerButton`] widget.
datepicker = ["chrono"] datepicker = ["chrono"]
@ -38,6 +38,14 @@ file = ["dep:mime_guess"]
## Add support for loading images via HTTP. ## Add support for loading images via HTTP.
http = ["dep:ehttp"] 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. ## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate.
## ##
## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. ## 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. ## Enable this when generating docs.
document-features = { version = "0.2", optional = true } 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 } image = { version = "0.24", optional = true, default-features = false }
# file feature # file feature

View File

@ -34,6 +34,8 @@ pub use crate::sizing::Size;
pub use crate::strip::*; pub use crate::strip::*;
pub use crate::table::*; pub use crate::table::*;
pub use loaders::install_image_loaders;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
mod profiling_scopes { mod profiling_scopes {

View File

@ -1,22 +1,25 @@
// TODO: automatic cache eviction // TODO: automatic cache eviction
/// Installs the default set of loaders. /// Installs a set of image loaders.
/// ///
/// - `file` loader on non-Wasm targets /// Calling this enables the use of [`egui::Image`] and [`egui::Ui::image`].
/// - `http` loader (with the `http` feature) ///
/// - `image` loader (with the `image` feature) /// ⚠ This will do nothing and you won't see any images unless you also enable some feature flags on `egui_extras`:
/// - `svg` loader with the `svg` feature ///
/// - `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. /// Calling this multiple times on the same [`egui::Context`] is safe.
/// It will never install duplicate loaders. /// 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) /// - 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: /// crate as your direct dependency, and enabling features on it:
/// ///
/// ```toml,ignore /// ```toml,ignore
/// egui_extras = { version = "*", features = ["all_loaders"] }
/// image = { version = "0.24", features = ["jpeg", "png"] } /// 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. /// 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`]. /// 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. /// 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 /// 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`], /// 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. /// 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. /// 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"))] #[cfg(all(not(target_arch = "wasm32"), feature = "file"))]
if !ctx.is_loader_installed(self::file_loader::FileLoader::ID) { if !ctx.is_loader_installed(self::file_loader::FileLoader::ID) {
ctx.add_bytes_loader(std::sync::Arc::new(self::file_loader::FileLoader::default())); 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 = "image"),
not(feature = "svg") 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; let _ = ctx;
} }

View File

@ -72,7 +72,7 @@ impl BytesLoader for EhttpLoader {
bytes: Bytes::Shared(file.bytes), bytes: Bytes::Shared(file.bytes),
mime: file.mime, 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 }), Poll::Pending => Ok(BytesPoll::Pending { size: None }),
} }
} else { } else {

View File

@ -45,7 +45,7 @@ impl BytesLoader for FileLoader {
bytes: Bytes::Shared(file.bytes), bytes: Bytes::Shared(file.bytes),
mime: file.mime, 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 }), Poll::Pending => Ok(BytesPoll::Pending { size: None }),
} }
} else { } else {

View File

@ -50,7 +50,7 @@ impl ImageLoader for ImageCrateLoader {
if let Some(entry) = cache.get(uri).cloned() { if let Some(entry) = cache.get(uri).cloned() {
match entry { match entry {
Ok(image) => Ok(ImagePoll::Ready { image }), Ok(image) => Ok(ImagePoll::Ready { image }),
Err(err) => Err(LoadError::Custom(err)), Err(err) => Err(LoadError::Loading(err)),
} }
} else { } else {
match ctx.try_load_bytes(uri) { match ctx.try_load_bytes(uri) {
@ -68,7 +68,7 @@ impl ImageLoader for ImageCrateLoader {
cache.insert(uri.into(), result.clone()); cache.insert(uri.into(), result.clone());
match result { match result {
Ok(image) => Ok(ImagePoll::Ready { image }), 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 }), Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }),

View File

@ -42,7 +42,7 @@ impl ImageLoader for SvgLoader {
if let Some(entry) = cache.get(&(uri.clone(), size_hint)).cloned() { if let Some(entry) = cache.get(&(uri.clone(), size_hint)).cloned() {
match entry { match entry {
Ok(image) => Ok(ImagePoll::Ready { image }), Ok(image) => Ok(ImagePoll::Ready { image }),
Err(err) => Err(LoadError::Custom(err)), Err(err) => Err(LoadError::Loading(err)),
} }
} else { } else {
match ctx.try_load_bytes(&uri) { match ctx.try_load_bytes(&uri) {
@ -60,7 +60,7 @@ impl ImageLoader for SvgLoader {
cache.insert((uri, size_hint), result.clone()); cache.insert((uri, size_hint), result.clone());
match result { match result {
Ok(image) => Ok(ImagePoll::Ready { image }), 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 }), Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }),

View File

@ -370,25 +370,6 @@ impl Painter {
Primitive::Callback(callback) => { Primitive::Callback(callback) => {
if callback.rect.is_positive() { if callback.rect.is_positive() {
crate::profile_scope!("callback"); 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 { let info = egui::PaintCallbackInfo {
viewport: callback.rect, viewport: callback.rect,
@ -397,6 +378,16 @@ impl Painter {
screen_size_px, 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::<CallbackFn>() { if let Some(callback) = callback.callback.downcast_ref::<CallbackFn>() {
(callback.f)(info, self); (callback.f)(info, self);
} else { } else {

View File

@ -787,6 +787,8 @@ pub struct PaintCallbackInfo {
/// Rect is the [-1, +1] of the Normalized Device Coordinates. /// 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`]. /// Note than only a portion of this may be visible due to [`Self::clip_rect`].
///
/// This comes from [`PaintCallback::rect`].
pub viewport: Rect, pub viewport: Rect,
/// Clip rectangle in points. /// Clip rectangle in points.
@ -819,7 +821,7 @@ pub struct ViewportInPixels {
} }
impl PaintCallbackInfo { impl PaintCallbackInfo {
fn points_to_pixels(&self, rect: &Rect) -> ViewportInPixels { fn pixels_from_points(&self, rect: &Rect) -> ViewportInPixels {
ViewportInPixels { ViewportInPixels {
left_px: rect.min.x * self.pixels_per_point, left_px: rect.min.x * self.pixels_per_point,
top_px: rect.min.y * 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`. /// The viewport rectangle. This is what you would use in e.g. `glViewport`.
pub fn viewport_in_pixels(&self) -> ViewportInPixels { 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`. /// The "scissor" or "clip" rectangle. This is what you would use in e.g. `glScissor`.
pub fn clip_rect_in_pixels(&self) -> ViewportInPixels { 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)] #[derive(Clone)]
pub struct PaintCallback { pub struct PaintCallback {
/// Where to paint. /// Where to paint.
///
/// This will become [`PaintCallbackInfo::viewport`].
pub rect: Rect, pub rect: Rect,
/// Paint something custom (e.g. 3D stuff). /// Paint something custom (e.g. 3D stuff).

View File

@ -1063,7 +1063,7 @@ fn mul_color(color: Color32, factor: f32) -> Color32 {
/// ///
/// For performance reasons it is smart to reuse the same [`Tessellator`]. /// 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 { pub struct Tessellator {
pixels_per_point: f32, pixels_per_point: f32,
options: TessellationOptions, options: TessellationOptions,

View File

@ -12,7 +12,7 @@ publish = false
eframe = { path = "../../crates/eframe", features = [ eframe = { path = "../../crates/eframe", features = [
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO "__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" env_logger = "0.10"
image = { version = "0.24", default-features = false, features = [ image = { version = "0.24", default-features = false, features = [
"jpeg", "jpeg",

View File

@ -1,12 +1,11 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::egui; use eframe::egui;
use eframe::epaint::vec2;
fn main() -> Result<(), eframe::Error> { fn main() -> Result<(), eframe::Error> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions { let options = eframe::NativeOptions {
drag_and_drop_support: true, initial_window_size: Some(egui::vec2(600.0, 800.0)),
..Default::default() ..Default::default()
}; };
eframe::run_native( eframe::run_native(
@ -14,7 +13,7 @@ fn main() -> Result<(), eframe::Error> {
options, options,
Box::new(|cc| { Box::new(|cc| {
// The following call is needed to load images when using `ui.image` and `egui::Image`: // 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::<MyApp>::default() Box::<MyApp>::default()
}), }),
) )
@ -27,10 +26,8 @@ impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::new([true, true]).show(ui, |ui| { egui::ScrollArea::new([true, true]).show(ui, |ui| {
ui.add( ui.image(egui::include_image!("ferris.svg"));
egui::Image::new(egui::include_image!("ferris.svg"))
.fit_to_fraction(vec2(1.0, 0.5)),
);
ui.add( ui.add(
egui::Image::new("https://picsum.photos/seed/1.759706314/1024") egui::Image::new("https://picsum.photos/seed/1.759706314/1024")
.rounding(egui::Rounding::same(10.0)), .rounding(egui::Rounding::same(10.0)),