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:
parent
e367c20779
commit
d7d222d3f6
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1121,9 +1121,16 @@ impl Context {
|
|||
|
||||
/// 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`].
|
||||
///
|
||||
|
|
@ -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<String>,
|
||||
|
|
@ -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<Cow<'static, str>>, bytes: impl Into<Bytes>) {
|
||||
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<dyn load::BytesLoader + Send + Sync + 'static>) {
|
||||
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<dyn load::ImageLoader + Send + Sync + 'static>) {
|
||||
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<dyn load::TextureLoader + Send + Sync + 'static>) {
|
||||
|
|
@ -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<Loaders> {
|
||||
/// The loaders of bytes, images, and textures.
|
||||
pub fn loaders(&self) -> Arc<Loaders> {
|
||||
crate::profile_function!();
|
||||
self.read(|this| this.loaders.clone())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
/// 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<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`.
|
||||
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<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)`.
|
||||
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<dyn ImageLoader + Send + Sync + 'static>;
|
|||
type TextureLoaderImpl = Arc<dyn TextureLoader + Send + Sync + 'static>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Loaders {
|
||||
/// The loaders of bytes, images, and textures.
|
||||
pub struct Loaders {
|
||||
pub include: Arc<DefaultBytesLoader>,
|
||||
pub bytes: Mutex<Vec<BytesLoaderImpl>>,
|
||||
pub image: Mutex<Vec<ImageLoaderImpl>>,
|
||||
|
|
|
|||
|
|
@ -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<HashMap<Cow<'static, str>, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<CursorIcon>,
|
||||
|
||||
/// 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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
|
||||
crate::Frame::group(self.style()).show(self, add_contents)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Image<'a>>,
|
||||
text: Option<WidgetText>,
|
||||
shortcut_text: WidgetText,
|
||||
wrap: Option<bool>,
|
||||
|
||||
|
|
@ -34,13 +33,30 @@ pub struct Button<'a> {
|
|||
frame: Option<bool>,
|
||||
min_size: Vec2,
|
||||
rounding: Option<Rounding>,
|
||||
image: Option<Image<'a>>,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl<'a> Button<'a> {
|
||||
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 {
|
||||
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<Image<'a>>, text: impl Into<WidgetText>) -> 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,31 +228,81 @@ 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);
|
||||
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 {
|
||||
let shortcut_text_pos = pos2(
|
||||
|
|
@ -228,29 +315,6 @@ impl Widget for Button<'_> {
|
|||
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 {
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Cow<'static, str>>, bytes: impl Into<Bytes>) -> 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<ImageSource<'a>>> From<T> 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>) -> 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 `<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`].
|
||||
///
|
||||
/// # 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<bool>,
|
||||
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()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
|
|
|
|||
|
|
@ -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::<CallbackFn>() {
|
||||
(callback.f)(info, self);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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::<MyApp>::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)),
|
||||
|
|
|
|||
Loading…
Reference in New Issue