Browse Source

Managed texture loading (#3297)

* add types from proposal

* add load methods on `egui::Context`

* implement loaders from proposal in `egui_extras`

* impl `From<Vec2>` for `SizeHint`

* re-export `SizeHint` from `egui` root

* rework `svg` example to use new managed `Image`

* split loaders into separate files + add logging

* add `log_trace`

* clean up `RetainedImage` from `svg` example

* refactor ehttp loader response to bytes mapping

* remove spammy trace

* load images even without extension

* fix lints

* remove unused imports

* use `Image2` in `download_image`

* use `visuals.error_fg_color` in `Image2` error state

* update lockfile

* use `Arc<ColorImage>` in `ImageData` + add `forget` API

* add `ui.image2`

* add byte size query api

* use iterators to sum loader byte sizes

* add static image loading

* use static image in `svg` example

* small refactor of `Image2::ui` texture loading code

* add `ImageFit` to size images properly

* remove println calls

* add bad image load to `download_image` example

* add loader file extension support tests

* fix lint errors in `loaders`

* remove unused `poll-promise` dependency

* add some docs to `Image2`

* add some docs to `egui_extras::loaders::install`

* explain `loaders::install` in examples

* fix lint

* upgrade `ehttp` to `0.3` for some crates

* Remove some unused dependencies

* Remove unnecessary context clone

* Turn on the `log` create feature of egui_extras in all examples

* rename `forget` and document it

* derive `Debug` on `SizeHint`

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>

* round when converting SizeHint from vec2

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>

* add `load` module docs

* docstring `add_loader` methods

* expose + document `load_include_bytes`

* cache texture handles in `DefaultTextureLoader`

* add `image2` doctest + further document `Image2`

* use `Default` for default `Image2` options

* update `image2` doc comment

* mention immediate-mode safety

* more fit calculation into inherent impl

* add hover text on spinner

* add `all-loaders` feature

* clarify `egui_extras::loaders::install` behavior

* explain how to enable image formats

* properly format `uri`

* use `thread::Builder` instead of `spawn`

* use eq op instead of `matches`

* inline `From<Arc<ColorImage>>` for `ImageData`

* allow non-`'static` bytes + `forget` in `DefaultTextureLoader`

* sort features

* change `ehttp` feature to `http`

* update `Image2` docs

* refactor loader cache type

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
pull/3314/head
Jan Procházka 1 year ago
committed by GitHub
parent
commit
ec671e754f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      Cargo.lock
  2. 155
      crates/egui/src/context.rs
  3. 2
      crates/egui/src/lib.rs
  4. 417
      crates/egui/src/load.rs
  5. 19
      crates/egui/src/ui.rs
  6. 221
      crates/egui/src/widgets/image.rs
  7. 2
      crates/egui/src/widgets/mod.rs
  8. 6
      crates/egui_demo_app/Cargo.toml
  9. 4
      crates/egui_demo_lib/Cargo.toml
  10. 10
      crates/egui_extras/Cargo.toml
  11. 29
      crates/egui_extras/src/lib.rs
  12. 58
      crates/egui_extras/src/loaders.rs
  13. 107
      crates/egui_extras/src/loaders/ehttp_loader.rs
  14. 84
      crates/egui_extras/src/loaders/file_loader.rs
  15. 84
      crates/egui_extras/src/loaders/image_loader.rs
  16. 92
      crates/egui_extras/src/loaders/svg_loader.rs
  17. 10
      crates/epaint/src/image.rs
  18. 5
      crates/epaint/src/texture_handle.rs
  19. 8
      examples/download_image/Cargo.toml
  20. 63
      examples/download_image/src/main.rs
  21. 2
      examples/retained_image/Cargo.toml
  22. 1
      examples/screenshot/Cargo.toml
  23. 2
      examples/svg/Cargo.toml
  24. 32
      examples/svg/src/main.rs

9
Cargo.lock

@ -1089,10 +1089,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"eframe", "eframe",
"egui_extras", "egui_extras",
"ehttp",
"env_logger", "env_logger",
"image", "image",
"poll-promise",
] ]
[[package]] [[package]]
@ -1263,6 +1261,7 @@ dependencies = [
"chrono", "chrono",
"document-features", "document-features",
"egui", "egui",
"ehttp",
"image", "image",
"log", "log",
"puffin", "puffin",
@ -1302,10 +1301,11 @@ dependencies = [
[[package]] [[package]]
name = "ehttp" name = "ehttp"
version = "0.2.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80b69a6f9168b96c0ae04763bec27a8b06b34343c334dd2703a4ec21f0f5e110" checksum = "31e4525e883dd283d12b755ab3ad71d7c8dea2ee8e8a062b9f4c4f84637ed681"
dependencies = [ dependencies = [
"document-features",
"js-sys", "js-sys",
"ureq", "ureq",
"wasm-bindgen", "wasm-bindgen",
@ -3166,7 +3166,6 @@ dependencies = [
"eframe", "eframe",
"env_logger", "env_logger",
"image", "image",
"itertools",
] ]
[[package]] [[package]]

155
crates/egui/src/context.rs

@ -166,6 +166,8 @@ struct ContextImpl {
is_accesskit_enabled: bool, is_accesskit_enabled: bool,
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
accesskit_node_classes: accesskit::NodeClassSet, accesskit_node_classes: accesskit::NodeClassSet,
loaders: load::Loaders,
} }
impl ContextImpl { impl ContextImpl {
@ -1901,6 +1903,159 @@ impl Context {
} }
} }
/// ## Image loading
impl Context {
/// Associate some static bytes with a `uri`.
///
/// The same `uri` may be passed to [`Ui::image2`] later to load the bytes as an image.
pub fn include_static_bytes(&self, uri: &'static str, bytes: &'static [u8]) {
self.read(|ctx| ctx.loaders.include.insert_static(uri, bytes));
}
/// Associate some bytes with a `uri`.
///
/// The same `uri` may be passed to [`Ui::image2`] later to load the bytes as an image.
pub fn include_bytes(&self, uri: &'static str, bytes: impl Into<Arc<[u8]>>) {
self.read(|ctx| ctx.loaders.include.insert_shared(uri, bytes));
}
/// Append an entry onto the chain of bytes loaders.
///
/// See [`load`] for more information.
pub fn add_bytes_loader(&self, loader: Arc<dyn load::BytesLoader + Send + Sync + 'static>) {
self.write(|ctx| ctx.loaders.bytes.push(loader));
}
/// Append an entry onto the chain of image loaders.
///
/// See [`load`] for more information.
pub fn add_image_loader(&self, loader: Arc<dyn load::ImageLoader + Send + Sync + 'static>) {
self.write(|ctx| ctx.loaders.image.push(loader));
}
/// Append an entry onto the chain of texture loaders.
///
/// See [`load`] for more information.
pub fn add_texture_loader(&self, loader: Arc<dyn load::TextureLoader + Send + Sync + 'static>) {
self.write(|ctx| ctx.loaders.texture.push(loader));
}
/// Release all memory and textures related to the given image URI.
///
/// If you attempt to load the image again, it will be reloaded from scratch.
pub fn forget_image(&self, uri: &str) {
self.write(|ctx| {
use crate::load::BytesLoader as _;
ctx.loaders.include.forget(uri);
for loader in &ctx.loaders.bytes {
loader.forget(uri);
}
for loader in &ctx.loaders.image {
loader.forget(uri);
}
for loader in &ctx.loaders.texture {
loader.forget(uri);
}
});
}
/// Try loading the bytes from the given uri using any available bytes loaders.
///
/// Loaders are expected to cache results, so that this call is immediate-mode safe.
///
/// This calls the loaders one by one in the order in which they were registered.
/// If a loader returns [`LoadError::NotSupported`][not_supported],
/// then the next loader is called. This process repeats until all loaders have
/// been exhausted, at which point this returns [`LoadError::NotSupported`][not_supported].
///
/// # 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.
///
/// [not_supported]: crate::load::LoadError::NotSupported
/// [custom]: crate::load::LoadError::Custom
pub fn try_load_bytes(&self, uri: &str) -> load::BytesLoadResult {
self.read(|this| {
for loader in &this.loaders.bytes {
match loader.load(self, uri) {
Err(load::LoadError::NotSupported) => continue,
result => return result,
}
}
Err(load::LoadError::NotSupported)
})
}
/// Try loading the image from the given uri using any available image loaders.
///
/// Loaders are expected to cache results, so that this call is immediate-mode safe.
///
/// This calls the loaders one by one in the order in which they were registered.
/// If a loader returns [`LoadError::NotSupported`][not_supported],
/// then the next loader is called. This process repeats until all loaders have
/// been exhausted, at which point this returns [`LoadError::NotSupported`][not_supported].
///
/// # 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.
///
/// [not_supported]: crate::load::LoadError::NotSupported
/// [custom]: crate::load::LoadError::Custom
pub fn try_load_image(&self, uri: &str, size_hint: load::SizeHint) -> load::ImageLoadResult {
self.read(|this| {
for loader in &this.loaders.image {
match loader.load(self, uri, size_hint) {
Err(load::LoadError::NotSupported) => continue,
result => return result,
}
}
Err(load::LoadError::NotSupported)
})
}
/// Try loading the texture from the given uri using any available texture loaders.
///
/// Loaders are expected to cache results, so that this call is immediate-mode safe.
///
/// This calls the loaders one by one in the order in which they were registered.
/// If a loader returns [`LoadError::NotSupported`][not_supported],
/// then the next loader is called. This process repeats until all loaders have
/// been exhausted, at which point this returns [`LoadError::NotSupported`][not_supported].
///
/// # 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.
///
/// [not_supported]: crate::load::LoadError::NotSupported
/// [custom]: crate::load::LoadError::Custom
pub fn try_load_texture(
&self,
uri: &str,
texture_options: TextureOptions,
size_hint: load::SizeHint,
) -> load::TextureLoadResult {
self.read(|this| {
for loader in &this.loaders.texture {
match loader.load(self, uri, texture_options, size_hint) {
Err(load::LoadError::NotSupported) => continue,
result => return result,
}
}
Err(load::LoadError::NotSupported)
})
}
}
#[test] #[test]
fn context_impl_send_sync() { fn context_impl_send_sync() {
fn assert_send_sync<T: Send + Sync>() {} fn assert_send_sync<T: Send + Sync>() {}

2
crates/egui/src/lib.rs

@ -314,6 +314,7 @@ mod input_state;
pub mod introspection; pub mod introspection;
pub mod layers; pub mod layers;
mod layout; mod layout;
pub mod load;
mod memory; mod memory;
pub mod menu; pub mod menu;
pub mod os; pub mod os;
@ -370,6 +371,7 @@ pub use {
input_state::{InputState, MultiTouchInfo, PointerState}, input_state::{InputState, MultiTouchInfo, PointerState},
layers::{LayerId, Order}, layers::{LayerId, Order},
layout::*, layout::*,
load::SizeHint,
memory::{Memory, Options}, memory::{Memory, Options},
painter::Painter, painter::Painter,
response::{InnerResponse, Response}, response::{InnerResponse, Response},

417
crates/egui/src/load.rs

@ -0,0 +1,417 @@
//! Types and traits related to image loading.
//!
//! If you just want to load some images, see [`egui_extras`](https://crates.io/crates/egui_extras/),
//! which contains reasonable default implementations of these traits. You can get started quickly
//! using [`egui_extras::loaders::install`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html).
//!
//! ## Loading process
//!
//! There are three kinds of loaders:
//! - [`BytesLoader`]: load the raw bytes of an image
//! - [`ImageLoader`]: decode the bytes into an array of colors
//! - [`TextureLoader`]: ask the backend to put an image onto the GPU
//!
//! The different kinds of loaders represent different layers in the loading process:
//!
//! ```text,ignore
//! ui.image2("file://image.png")
//! └► ctx.try_load_texture("file://image.png", ...)
//! └► TextureLoader::load("file://image.png", ...)
//! └► ctx.try_load_image("file://image.png", ...)
//! └► ImageLoader::load("file://image.png", ...)
//! └► ctx.try_load_bytes("file://image.png", ...)
//! └► BytesLoader::load("file://image.png", ...)
//! ```
//!
//! As each layer attempts to load the URI, it first asks the layer below it
//! for the data it needs to do its job. But this is not a strict requirement,
//! an implementation could instead generate the data it needs!
//!
//! Loader trait implementations may be registered on a context with:
//! - [`Context::add_bytes_loader`]
//! - [`Context::add_image_loader`]
//! - [`Context::add_texture_loader`]
//!
//! There may be multiple loaders of the same kind registered at the same time.
//! The `try_load` methods on [`Context`] will attempt to call each loader one by one,
//! until one of them returns something other than [`LoadError::NotSupported`].
//!
//! The loaders are stored in the context. This means they may hold state across frames,
//! which they can (and _should_) use to cache the results of the operations they perform.
//!
//! For example, a [`BytesLoader`] that loads file URIs (`file://image.png`)
//! would cache each file read. A [`TextureLoader`] would cache each combination
//! of `(URI, TextureOptions)`, and so on.
//!
//! Each URI will be passed through the loaders as a plain `&str`.
//! The loaders are free to derive as much meaning from the URI as they wish to.
//! For example, a loader may determine that it doesn't support loading a specific URI
//! if the protocol does not match what it expects.
use crate::Context;
use ahash::HashMap;
use epaint::mutex::Mutex;
use epaint::TextureHandle;
use epaint::{textures::TextureOptions, ColorImage, TextureId, Vec2};
use std::ops::Deref;
use std::{error::Error as StdError, fmt::Display, sync::Arc};
#[derive(Clone, Debug)]
pub enum LoadError {
/// This loader does not support this protocol or image format.
NotSupported,
/// A custom error message (e.g. "File not found: foo.png").
Custom(String),
}
impl Display for LoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LoadError::NotSupported => f.write_str("not supported"),
LoadError::Custom(message) => f.write_str(message),
}
}
}
impl StdError for LoadError {}
pub type Result<T, E = LoadError> = std::result::Result<T, E>;
/// Given as a hint for image loading requests.
///
/// Used mostly for rendering SVG:s to a good size.
///
/// All variants will preserve the original aspect ratio.
///
/// Similar to `usvg::FitTo`.
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SizeHint {
/// Keep original size.
#[default]
Original,
/// Scale to width.
Width(u32),
/// Scale to height.
Height(u32),
/// Scale to size.
Size(u32, u32),
}
impl From<Vec2> for SizeHint {
fn from(value: Vec2) -> Self {
Self::Size(value.x.round() as u32, value.y.round() as u32)
}
}
// TODO: API for querying bytes caches in each loader
pub type Size = [usize; 2];
#[derive(Clone)]
pub enum Bytes {
Static(&'static [u8]),
Shared(Arc<[u8]>),
}
impl From<&'static [u8]> for Bytes {
#[inline]
fn from(value: &'static [u8]) -> Self {
Bytes::Static(value)
}
}
impl From<Arc<[u8]>> for Bytes {
#[inline]
fn from(value: Arc<[u8]>) -> Self {
Bytes::Shared(value)
}
}
impl AsRef<[u8]> for Bytes {
#[inline]
fn as_ref(&self) -> &[u8] {
match self {
Bytes::Static(bytes) => bytes,
Bytes::Shared(bytes) => bytes,
}
}
}
impl Deref for Bytes {
type Target = [u8];
#[inline]
fn deref(&self) -> &Self::Target {
self.as_ref()
}
}
#[derive(Clone)]
pub enum BytesPoll {
/// Bytes are being loaded.
Pending {
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Size>,
},
/// Bytes are loaded.
Ready {
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Size>,
/// File contents, e.g. the contents of a `.png`.
bytes: Bytes,
},
}
pub type BytesLoadResult = Result<BytesPoll>;
pub trait BytesLoader {
/// Try loading the bytes from the given uri.
///
/// Implementations should call `ctx.request_repaint` to wake up the ui
/// once the data is ready.
///
/// The implementation should cache any result, so that calling this
/// is immediate-mode safe.
///
/// # Errors
/// This may fail with:
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
/// - [`LoadError::Custom`] if the loading process failed.
fn load(&self, ctx: &Context, uri: &str) -> BytesLoadResult;
/// Forget the given `uri`.
///
/// If `uri` is cached, it should be evicted from cache,
/// so that it may be fully reloaded.
fn forget(&self, uri: &str);
/// Implementations may use this to perform work at the end of a frame,
/// such as evicting unused entries from a cache.
fn end_frame(&self, frame_index: usize) {
let _ = frame_index;
}
/// If the loader caches any data, this should return the size of that cache.
fn byte_size(&self) -> usize;
}
#[derive(Clone)]
pub enum ImagePoll {
/// Image is loading.
Pending {
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Size>,
},
/// Image is loaded.
Ready { image: Arc<ColorImage> },
}
pub type ImageLoadResult = Result<ImagePoll>;
pub trait ImageLoader {
/// Try loading the image from the given uri.
///
/// Implementations should call `ctx.request_repaint` to wake up the ui
/// once the image is ready.
///
/// The implementation should cache any result, so that calling this
/// is immediate-mode safe.
///
/// # Errors
/// This may fail with:
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
/// - [`LoadError::Custom`] if the loading process failed.
fn load(&self, ctx: &Context, uri: &str, size_hint: SizeHint) -> ImageLoadResult;
/// Forget the given `uri`.
///
/// If `uri` is cached, it should be evicted from cache,
/// so that it may be fully reloaded.
fn forget(&self, uri: &str);
/// Implementations may use this to perform work at the end of a frame,
/// such as evicting unused entries from a cache.
fn end_frame(&self, frame_index: usize) {
let _ = frame_index;
}
/// If the loader caches any data, this should return the size of that cache.
fn byte_size(&self) -> usize;
}
/// A texture with a known size.
#[derive(Clone)]
pub struct SizedTexture {
pub id: TextureId,
pub size: Size,
}
impl SizedTexture {
pub fn from_handle(handle: &TextureHandle) -> Self {
Self {
id: handle.id(),
size: handle.size(),
}
}
}
#[derive(Clone)]
pub enum TexturePoll {
/// Texture is loading.
Pending {
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Size>,
},
/// Texture is loaded.
Ready { texture: SizedTexture },
}
pub type TextureLoadResult = Result<TexturePoll>;
pub trait TextureLoader {
/// Try loading the texture from the given uri.
///
/// Implementations should call `ctx.request_repaint` to wake up the ui
/// once the texture is ready.
///
/// The implementation should cache any result, so that calling this
/// is immediate-mode safe.
///
/// # Errors
/// This may fail with:
/// - [`LoadError::NotSupported`] if the loader does not support loading `uri`.
/// - [`LoadError::Custom`] if the loading process failed.
fn load(
&self,
ctx: &Context,
uri: &str,
texture_options: TextureOptions,
size_hint: SizeHint,
) -> TextureLoadResult;
/// Forget the given `uri`.
///
/// If `uri` is cached, it should be evicted from cache,
/// so that it may be fully reloaded.
fn forget(&self, uri: &str);
/// Implementations may use this to perform work at the end of a frame,
/// such as evicting unused entries from a cache.
fn end_frame(&self, frame_index: usize) {
let _ = frame_index;
}
/// If the loader caches any data, this should return the size of that cache.
fn byte_size(&self) -> usize;
}
#[derive(Default)]
pub(crate) struct DefaultBytesLoader {
cache: Mutex<HashMap<&'static str, Bytes>>,
}
impl DefaultBytesLoader {
pub(crate) fn insert_static(&self, uri: &'static str, bytes: &'static [u8]) {
self.cache
.lock()
.entry(uri)
.or_insert_with(|| Bytes::Static(bytes));
}
pub(crate) fn insert_shared(&self, uri: &'static str, bytes: impl Into<Arc<[u8]>>) {
self.cache
.lock()
.entry(uri)
.or_insert_with(|| Bytes::Shared(bytes.into()));
}
}
impl BytesLoader for DefaultBytesLoader {
fn load(&self, _: &Context, uri: &str) -> BytesLoadResult {
match self.cache.lock().get(uri).cloned() {
Some(bytes) => Ok(BytesPoll::Ready { size: None, bytes }),
None => Err(LoadError::NotSupported),
}
}
fn forget(&self, uri: &str) {
let _ = self.cache.lock().remove(uri);
}
fn byte_size(&self) -> usize {
self.cache.lock().values().map(|bytes| bytes.len()).sum()
}
}
#[derive(Default)]
struct DefaultTextureLoader {
cache: Mutex<HashMap<(String, TextureOptions), TextureHandle>>,
}
impl TextureLoader for DefaultTextureLoader {
fn load(
&self,
ctx: &Context,
uri: &str,
texture_options: TextureOptions,
size_hint: SizeHint,
) -> TextureLoadResult {
let mut cache = self.cache.lock();
if let Some(handle) = cache.get(&(uri.into(), texture_options)) {
let texture = SizedTexture::from_handle(handle);
Ok(TexturePoll::Ready { texture })
} else {
match ctx.try_load_image(uri, size_hint)? {
ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }),
ImagePoll::Ready { image } => {
let handle = ctx.load_texture(uri, image, texture_options);
let texture = SizedTexture::from_handle(&handle);
cache.insert((uri.into(), texture_options), handle);
Ok(TexturePoll::Ready { texture })
}
}
}
}
fn forget(&self, uri: &str) {
self.cache.lock().retain(|(u, _), _| u != uri);
}
fn end_frame(&self, _: usize) {}
fn byte_size(&self) -> usize {
self.cache
.lock()
.values()
.map(|texture| texture.byte_size())
.sum()
}
}
pub(crate) struct Loaders {
pub include: Arc<DefaultBytesLoader>,
pub bytes: Vec<Arc<dyn BytesLoader + Send + Sync + 'static>>,
pub image: Vec<Arc<dyn ImageLoader + Send + Sync + 'static>>,
pub texture: Vec<Arc<dyn TextureLoader + Send + Sync + 'static>>,
}
impl Default for Loaders {
fn default() -> Self {
let include = Arc::new(DefaultBytesLoader::default());
Self {
bytes: vec![include.clone()],
image: Vec::new(),
// By default we only include `DefaultTextureLoader`.
texture: vec![Arc::new(DefaultTextureLoader::default())],
include,
}
}
}

19
crates/egui/src/ui.rs

@ -1590,6 +1590,25 @@ impl Ui {
pub fn image(&mut self, texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Response { pub fn image(&mut self, texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Response {
Image::new(texture_id, size).ui(self) Image::new(texture_id, size).ui(self)
} }
/// 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 loaders handle caching image data, sampled textures, etc. across frames, so calling this is immediate-mode safe.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.image2("file://ferris.svg");
/// # });
/// ```
///
/// See also [`crate::Image2`] and [`crate::ImageSource`].
#[inline]
pub fn image2<'a>(&mut self, source: impl Into<ImageSource<'a>>) -> Response {
Image2::new(source.into()).ui(self)
}
} }
/// # Colors /// # Colors

221
crates/egui/src/widgets/image.rs

@ -1,4 +1,7 @@
use crate::*; use std::sync::Arc;
use crate::load::Bytes;
use crate::{load::SizeHint, load::TexturePoll, *};
use emath::Rot2; use emath::Rot2;
/// An widget to show an image of a given size. /// An widget to show an image of a given size.
@ -173,3 +176,219 @@ impl Widget for Image {
response response
} }
} }
/// A widget which displays an image.
///
/// There are three ways to construct this widget:
/// - [`Image2::from_uri`]
/// - [`Image2::from_bytes`]
/// - [`Image2::from_static_bytes`]
///
/// In both cases the task of actually loading the image
/// is deferred to when the `Image2` is added to the [`Ui`].
///
/// See [`crate::load`] for more information.
pub struct Image2<'a> {
source: ImageSource<'a>,
texture_options: TextureOptions,
size_hint: SizeHint,
fit: ImageFit,
sense: Sense,
}
#[derive(Default, Clone, Copy)]
enum ImageFit {
// TODO: options for aspect ratio
// TODO: other fit strategies
// FitToWidth,
// FitToHeight,
// FitToWidthExact(f32),
// FitToHeightExact(f32),
#[default]
ShrinkToFit,
}
impl ImageFit {
pub fn calculate_final_size(&self, available_size: Vec2, image_size: Vec2) -> Vec2 {
let aspect_ratio = image_size.x / image_size.y;
// TODO: more image sizing options
match self {
// ImageFit::FitToWidth => todo!(),
// ImageFit::FitToHeight => todo!(),
// ImageFit::FitToWidthExact(_) => todo!(),
// ImageFit::FitToHeightExact(_) => todo!(),
ImageFit::ShrinkToFit => {
let width = if available_size.x < image_size.x {
available_size.x
} else {
image_size.x
};
let height = if available_size.y < image_size.y {
available_size.y
} else {
image_size.y
};
if width < height {
Vec2::new(width, width / aspect_ratio)
} else {
Vec2::new(height * aspect_ratio, height)
}
}
}
}
}
/// This type tells the [`Ui`] how to load the image.
pub enum ImageSource<'a> {
/// Load the image from a URI.
///
/// This could be a `file://` url, `http://` url, or a `bare` identifier.
/// How the URI will be turned into a texture for rendering purposes is
/// up to the registered loaders to handle.
///
/// See [`crate::load`] for more information.
Uri(&'a str),
/// Load the image from some raw bytes.
///
/// The [`Bytes`] may be:
/// - `'static`, obtained from `include_bytes!` or similar
/// - Anything that can be converted to `Arc<[u8]>`
///
/// This instructs the [`Ui`] to cache the raw bytes, which are then further processed by any registered loaders.
///
/// See [`crate::load`] for more information.
Bytes(&'static str, Bytes),
}
impl<'a> From<&'a str> for ImageSource<'a> {
#[inline]
fn from(value: &'a str) -> Self {
Self::Uri(value)
}
}
impl<T: Into<Bytes>> From<(&'static str, T)> for ImageSource<'static> {
#[inline]
fn from((uri, bytes): (&'static str, T)) -> Self {
Self::Bytes(uri, bytes.into())
}
}
impl<'a> Image2<'a> {
/// Load the image from some source.
pub fn new(source: ImageSource<'a>) -> Self {
Self {
source,
texture_options: Default::default(),
size_hint: Default::default(),
fit: Default::default(),
sense: Sense::hover(),
}
}
/// Load the image from a URI.
///
/// See [`ImageSource::Uri`].
pub fn from_uri(uri: &'a str) -> Self {
Self {
source: ImageSource::Uri(uri),
texture_options: Default::default(),
size_hint: Default::default(),
fit: Default::default(),
sense: Sense::hover(),
}
}
/// Load the image from some raw `'static` bytes.
///
/// For example, you can use this to load an image from bytes obtained via [`include_bytes`].
///
/// See [`ImageSource::Bytes`].
pub fn from_static_bytes(name: &'static str, bytes: &'static [u8]) -> Self {
Self {
source: ImageSource::Bytes(name, Bytes::Static(bytes)),
texture_options: Default::default(),
size_hint: Default::default(),
fit: Default::default(),
sense: Sense::hover(),
}
}
/// Load the image from some raw bytes.
///
/// See [`ImageSource::Bytes`].
pub fn from_bytes(name: &'static str, bytes: impl Into<Arc<[u8]>>) -> Self {
Self {
source: ImageSource::Bytes(name, Bytes::Shared(bytes.into())),
texture_options: Default::default(),
size_hint: Default::default(),
fit: Default::default(),
sense: Sense::hover(),
}
}
/// Texture options used when creating the texture.
#[inline]
pub fn texture_options(mut self, texture_options: TextureOptions) -> Self {
self.texture_options = texture_options;
self
}
/// Size hint used when creating the texture.
#[inline]
pub fn size_hint(mut self, size_hint: impl Into<SizeHint>) -> Self {
self.size_hint = size_hint.into();
self
}
/// Make the image respond to clicks and/or drags.
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
}
impl<'a> Widget for Image2<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let uri = match self.source {
ImageSource::Uri(uri) => uri,
ImageSource::Bytes(uri, bytes) => {
match bytes {
Bytes::Static(bytes) => ui.ctx().include_static_bytes(uri, bytes),
Bytes::Shared(bytes) => ui.ctx().include_bytes(uri, bytes),
}
uri
}
};
match ui
.ctx()
.try_load_texture(uri, self.texture_options, self.size_hint)
{
Ok(TexturePoll::Ready { texture }) => {
let final_size = self.fit.calculate_final_size(
ui.available_size(),
Vec2::new(texture.size[0] as f32, texture.size[1] as f32),
);
let (rect, response) = ui.allocate_exact_size(final_size, self.sense);
let mut mesh = Mesh::with_texture(texture.id);
mesh.add_rect_with_uv(
rect,
Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
Color32::WHITE,
);
ui.painter().add(Shape::mesh(mesh));
response
}
Ok(TexturePoll::Pending { .. }) => {
ui.spinner().on_hover_text(format!("Loading {uri:?}…"))
}
Err(err) => ui.colored_label(ui.visuals().error_fg_color, err.to_string()),
}
}
}

2
crates/egui/src/widgets/mod.rs

@ -22,7 +22,7 @@ pub mod text_edit;
pub use button::*; pub use button::*;
pub use drag_value::DragValue; pub use drag_value::DragValue;
pub use hyperlink::*; pub use hyperlink::*;
pub use image::Image; pub use image::{Image, Image2, ImageSource};
pub use label::*; pub use label::*;
pub use progress_bar::ProgressBar; pub use progress_bar::ProgressBar;
pub use selected_label::SelectableLabel; pub use selected_label::SelectableLabel;

6
crates/egui_demo_app/Cargo.toml

@ -45,10 +45,12 @@ log = { version = "0.4", features = ["std"] }
# Optional dependencies: # Optional dependencies:
bytemuck = { version = "1.7.1", optional = true } bytemuck = { version = "1.7.1", optional = true }
egui_extras = { version = "0.22.0", optional = true, path = "../egui_extras" } egui_extras = { version = "0.22.0", optional = true, path = "../egui_extras", features = [
"log",
] }
# feature "http": # feature "http":
ehttp = { version = "0.2.0", optional = true } ehttp = { version = "0.3.0", optional = true }
image = { version = "0.24", optional = true, default-features = false, features = [ image = { version = "0.24", optional = true, default-features = false, features = [
"jpeg", "jpeg",
"png", "png",

4
crates/egui_demo_lib/Cargo.toml

@ -33,7 +33,9 @@ syntax_highlighting = ["syntect"]
[dependencies] [dependencies]
egui = { version = "0.22.0", path = "../egui", default-features = false } egui = { version = "0.22.0", path = "../egui", default-features = false }
egui_extras = { version = "0.22.0", path = "../egui_extras" } egui_extras = { version = "0.22.0", path = "../egui_extras", features = [
"log",
] }
egui_plot = { version = "0.22.0", path = "../egui_plot" } egui_plot = { version = "0.22.0", path = "../egui_plot" }
enum-map = { version = "2", features = ["serde"] } enum-map = { version = "2", features = ["serde"] }
log = { version = "0.4", features = ["std"] } log = { version = "0.4", features = ["std"] }

10
crates/egui_extras/Cargo.toml

@ -26,12 +26,18 @@ all-features = true
[features] [features]
default = [] default = []
## Shorthand for enabling `svg`, `image`, and `ehttp`.
all-loaders = ["svg", "image", "http"]
## Enable [`DatePickerButton`] widget. ## Enable [`DatePickerButton`] widget.
datepicker = ["chrono"] datepicker = ["chrono"]
## Log warnings using [`log`](https://docs.rs/log) crate. ## Log warnings using [`log`](https://docs.rs/log) crate.
log = ["dep:log", "egui/log"] log = ["dep:log", "egui/log"]
## Add support for loading images via HTTP.
http = ["dep:ehttp"]
## 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.
@ -40,7 +46,6 @@ puffin = ["dep:puffin", "egui/puffin"]
## Support loading svg images. ## Support loading svg images.
svg = ["resvg", "tiny-skia", "usvg"] svg = ["resvg", "tiny-skia", "usvg"]
[dependencies] [dependencies]
egui = { version = "0.22.0", path = "../egui", default-features = false } egui = { version = "0.22.0", path = "../egui", default-features = false }
@ -76,3 +81,6 @@ puffin = { version = "0.16", optional = true }
resvg = { version = "0.28", optional = true, default-features = false } resvg = { version = "0.28", optional = true, default-features = false }
tiny-skia = { version = "0.8", optional = true, default-features = false } # must be updated in lock-step with resvg tiny-skia = { version = "0.8", optional = true, default-features = false } # must be updated in lock-step with resvg
usvg = { version = "0.28", optional = true, default-features = false } usvg = { version = "0.28", optional = true, default-features = false }
# http feature
ehttp = { version = "0.3.0", optional = true, default-features = false }

29
crates/egui_extras/src/lib.rs

@ -15,6 +15,7 @@ mod datepicker;
pub mod image; pub mod image;
mod layout; mod layout;
pub mod loaders;
mod sizing; mod sizing;
mod strip; mod strip;
mod table; mod table;
@ -85,3 +86,31 @@ macro_rules! log_or_panic {
}}; }};
} }
pub(crate) use log_or_panic; pub(crate) use log_or_panic;
#[allow(unused_macros)]
macro_rules! log_warn {
($fmt: literal) => {$crate::log_warn!($fmt,)};
($fmt: literal, $($arg: tt)*) => {{
#[cfg(feature = "log")]
log::warn!($fmt, $($arg)*);
#[cfg(not(feature = "log"))]
println!(
concat!("egui_extras: warning: ", $fmt), $($arg)*
)
}};
}
#[allow(unused_imports)]
pub(crate) use log_warn;
#[allow(unused_macros)]
macro_rules! log_trace {
($fmt: literal) => {$crate::log_trace!($fmt,)};
($fmt: literal, $($arg: tt)*) => {{
#[cfg(feature = "log")]
log::trace!($fmt, $($arg)*);
}};
}
#[allow(unused_imports)]
pub(crate) use log_trace;

58
crates/egui_extras/src/loaders.rs

@ -0,0 +1,58 @@
// TODO: automatic cache eviction
/// Installs the default set of loaders:
/// - `file` loader on non-Wasm targets
/// - `http` loader (with the `ehttp` feature)
/// - `image` loader (with the `image` feature)
/// - `svg` loader with the `svg` feature
///
/// ⚠ 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://` images, 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
/// image = { version = "0.24", features = ["jpeg", "png"] }
/// ```
///
/// See [`egui::load`] for more information about how loaders work.
pub fn install(ctx: &egui::Context) {
#[cfg(not(target_arch = "wasm32"))]
ctx.add_bytes_loader(std::sync::Arc::new(self::file_loader::FileLoader::default()));
#[cfg(feature = "http")]
ctx.add_bytes_loader(std::sync::Arc::new(
self::ehttp_loader::EhttpLoader::default(),
));
#[cfg(feature = "image")]
ctx.add_image_loader(std::sync::Arc::new(
self::image_loader::ImageCrateLoader::default(),
));
#[cfg(feature = "svg")]
ctx.add_image_loader(std::sync::Arc::new(self::svg_loader::SvgLoader::default()));
#[cfg(all(
target_arch = "wasm32",
not(feature = "http"),
not(feature = "image"),
not(feature = "svg")
))]
crate::log_warn!("`loaders::install` was called, but no loaders are enabled");
let _ = ctx;
}
#[cfg(not(target_arch = "wasm32"))]
mod file_loader;
#[cfg(feature = "http")]
mod ehttp_loader;
#[cfg(feature = "image")]
mod image_loader;
#[cfg(feature = "svg")]
mod svg_loader;

107
crates/egui_extras/src/loaders/ehttp_loader.rs

@ -0,0 +1,107 @@
use egui::{
ahash::HashMap,
load::{Bytes, BytesLoadResult, BytesLoader, BytesPoll, LoadError},
mutex::Mutex,
};
use std::{sync::Arc, task::Poll};
type Entry = Poll<Result<Arc<[u8]>, String>>;
#[derive(Default)]
pub struct EhttpLoader {
cache: Arc<Mutex<HashMap<String, Entry>>>,
}
const PROTOCOLS: &[&str] = &["http://", "https://"];
fn starts_with_one_of(s: &str, prefixes: &[&str]) -> bool {
prefixes.iter().any(|prefix| s.starts_with(prefix))
}
fn get_image_bytes(
uri: &str,
response: Result<ehttp::Response, String>,
) -> Result<Arc<[u8]>, String> {
let response = response?;
if !response.ok {
match response.text() {
Some(response_text) => {
return Err(format!(
"failed to load {uri:?}: {} {} {response_text}",
response.status, response.status_text
))
}
None => {
return Err(format!(
"failed to load {uri:?}: {} {}",
response.status, response.status_text
))
}
}
}
let Some(content_type) = response.content_type() else {
return Err(format!("failed to load {uri:?}: no content-type header found"));
};
if !content_type.starts_with("image/") {
return Err(format!("failed to load {uri:?}: expected content-type starting with \"image/\", found {content_type:?}"));
}
Ok(response.bytes.into())
}
impl BytesLoader for EhttpLoader {
fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult {
if !starts_with_one_of(uri, PROTOCOLS) {
return Err(LoadError::NotSupported);
}
let mut cache = self.cache.lock();
if let Some(entry) = cache.get(uri).cloned() {
match entry {
Poll::Ready(Ok(bytes)) => Ok(BytesPoll::Ready {
size: None,
bytes: Bytes::Shared(bytes),
}),
Poll::Ready(Err(err)) => Err(LoadError::Custom(err)),
Poll::Pending => Ok(BytesPoll::Pending { size: None }),
}
} else {
crate::log_trace!("started loading {uri:?}");
let uri = uri.to_owned();
cache.insert(uri.clone(), Poll::Pending);
drop(cache);
ehttp::fetch(ehttp::Request::get(uri.clone()), {
let ctx = ctx.clone();
let cache = self.cache.clone();
move |response| {
let result = get_image_bytes(&uri, response);
crate::log_trace!("finished loading {uri:?}");
let prev = cache.lock().insert(uri, Poll::Ready(result));
assert!(matches!(prev, Some(Poll::Pending)));
ctx.request_repaint();
}
});
Ok(BytesPoll::Pending { size: None })
}
}
fn forget(&self, uri: &str) {
let _ = self.cache.lock().remove(uri);
}
fn byte_size(&self) -> usize {
self.cache
.lock()
.values()
.map(|entry| match entry {
Poll::Ready(Ok(bytes)) => bytes.len(),
Poll::Ready(Err(err)) => err.len(),
_ => 0,
})
.sum()
}
}

84
crates/egui_extras/src/loaders/file_loader.rs

@ -0,0 +1,84 @@
use egui::{
ahash::HashMap,
load::{Bytes, BytesLoadResult, BytesLoader, BytesPoll, LoadError},
mutex::Mutex,
};
use std::{sync::Arc, task::Poll, thread};
type Entry = Poll<Result<Arc<[u8]>, String>>;
#[derive(Default)]
pub struct FileLoader {
/// Cache for loaded files
cache: Arc<Mutex<HashMap<String, Entry>>>,
}
const PROTOCOL: &str = "file://";
impl BytesLoader for FileLoader {
fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult {
// File loader only supports the `file` protocol.
let Some(path) = uri.strip_prefix(PROTOCOL) else {
return Err(LoadError::NotSupported);
};
let mut cache = self.cache.lock();
if let Some(entry) = cache.get(path).cloned() {
// `path` has either begun loading, is loaded, or has failed to load.
match entry {
Poll::Ready(Ok(bytes)) => Ok(BytesPoll::Ready {
size: None,
bytes: Bytes::Shared(bytes),
}),
Poll::Ready(Err(err)) => Err(LoadError::Custom(err)),
Poll::Pending => Ok(BytesPoll::Pending { size: None }),
}
} else {
crate::log_trace!("started loading {uri:?}");
// We need to load the file at `path`.
// Set the file to `pending` until we finish loading it.
let path = path.to_owned();
cache.insert(path.clone(), Poll::Pending);
drop(cache);
// Spawn a thread to read the file, so that we don't block the render for too long.
thread::Builder::new()
.name(format!("egui_extras::FileLoader::load({uri:?})"))
.spawn({
let ctx = ctx.clone();
let cache = self.cache.clone();
let uri = uri.to_owned();
move || {
let result = match std::fs::read(&path) {
Ok(bytes) => Ok(bytes.into()),
Err(err) => Err(err.to_string()),
};
let prev = cache.lock().insert(path, Poll::Ready(result));
assert!(matches!(prev, Some(Poll::Pending)));
ctx.request_repaint();
crate::log_trace!("finished loading {uri:?}");
}
})
.expect("failed to spawn thread");
Ok(BytesPoll::Pending { size: None })
}
}
fn forget(&self, uri: &str) {
let _ = self.cache.lock().remove(uri);
}
fn byte_size(&self) -> usize {
self.cache
.lock()
.values()
.map(|entry| match entry {
Poll::Ready(Ok(bytes)) => bytes.len(),
Poll::Ready(Err(err)) => err.len(),
_ => 0,
})
.sum()
}
}

84
crates/egui_extras/src/loaders/image_loader.rs

@ -0,0 +1,84 @@
use egui::{
ahash::HashMap,
load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint},
mutex::Mutex,
ColorImage,
};
use std::{mem::size_of, path::Path, sync::Arc};
type Entry = Result<Arc<ColorImage>, String>;
#[derive(Default)]
pub struct ImageCrateLoader {
cache: Mutex<HashMap<String, Entry>>,
}
fn is_supported(uri: &str) -> bool {
let Some(ext) = Path::new(uri).extension().and_then(|ext| ext.to_str()) else {
// `true` because if there's no extension, assume that we support it
return true
};
ext != "svg"
}
impl ImageLoader for ImageCrateLoader {
fn load(&self, ctx: &egui::Context, uri: &str, _: SizeHint) -> ImageLoadResult {
if !is_supported(uri) {
return Err(LoadError::NotSupported);
}
let mut cache = self.cache.lock();
if let Some(entry) = cache.get(uri).cloned() {
match entry {
Ok(image) => Ok(ImagePoll::Ready { image }),
Err(err) => Err(LoadError::Custom(err)),
}
} else {
match ctx.try_load_bytes(uri) {
Ok(BytesPoll::Ready { bytes, .. }) => {
crate::log_trace!("started loading {uri:?}");
let result = crate::image::load_image_bytes(&bytes).map(Arc::new);
crate::log_trace!("finished loading {uri:?}");
cache.insert(uri.into(), result.clone());
match result {
Ok(image) => Ok(ImagePoll::Ready { image }),
Err(err) => Err(LoadError::Custom(err)),
}
}
Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }),
Err(err) => Err(err),
}
}
}
fn forget(&self, uri: &str) {
let _ = self.cache.lock().remove(uri);
}
fn byte_size(&self) -> usize {
self.cache
.lock()
.values()
.map(|result| match result {
Ok(image) => image.pixels.len() * size_of::<egui::Color32>(),
Err(err) => err.len(),
})
.sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_support() {
assert!(is_supported("https://test.png"));
assert!(is_supported("test.jpeg"));
assert!(is_supported("http://test.gif"));
assert!(is_supported("test.webp"));
assert!(is_supported("file://test"));
assert!(!is_supported("test.svg"));
}
}

92
crates/egui_extras/src/loaders/svg_loader.rs

@ -0,0 +1,92 @@
use egui::{
ahash::HashMap,
load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint},
mutex::Mutex,
ColorImage,
};
use std::{mem::size_of, path::Path, sync::Arc};
type Entry = Result<Arc<ColorImage>, String>;
#[derive(Default)]
pub struct SvgLoader {
cache: Mutex<HashMap<(String, SizeHint), Entry>>,
}
fn is_supported(uri: &str) -> bool {
let Some(ext) = Path::new(uri).extension().and_then(|ext| ext.to_str()) else { return false };
ext == "svg"
}
impl ImageLoader for SvgLoader {
fn load(&self, ctx: &egui::Context, uri: &str, size_hint: SizeHint) -> ImageLoadResult {
if !is_supported(uri) {
return Err(LoadError::NotSupported);
}
let uri = uri.to_owned();
let mut cache = self.cache.lock();
// We can't avoid the `uri` clone here without unsafe code.
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)),
}
} else {
match ctx.try_load_bytes(&uri) {
Ok(BytesPoll::Ready { bytes, .. }) => {
crate::log_trace!("started loading {uri:?}");
let fit_to = match size_hint {
SizeHint::Original => usvg::FitTo::Original,
SizeHint::Width(w) => usvg::FitTo::Width(w),
SizeHint::Height(h) => usvg::FitTo::Height(h),
SizeHint::Size(w, h) => usvg::FitTo::Size(w, h),
};
let result =
crate::image::load_svg_bytes_with_size(&bytes, fit_to).map(Arc::new);
crate::log_trace!("finished loading {uri:?}");
cache.insert((uri, size_hint), result.clone());
match result {
Ok(image) => Ok(ImagePoll::Ready { image }),
Err(err) => Err(LoadError::Custom(err)),
}
}
Ok(BytesPoll::Pending { size }) => Ok(ImagePoll::Pending { size }),
Err(err) => Err(err),
}
}
}
fn forget(&self, uri: &str) {
self.cache.lock().retain(|(u, _), _| u != uri);
}
fn byte_size(&self) -> usize {
self.cache
.lock()
.values()
.map(|result| match result {
Ok(image) => image.pixels.len() * size_of::<egui::Color32>(),
Err(err) => err.len(),
})
.sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_support() {
// inverse of same test in `image_loader.rs`
assert!(!is_supported("https://test.png"));
assert!(!is_supported("test.jpeg"));
assert!(!is_supported("http://test.gif"));
assert!(!is_supported("test.webp"));
assert!(!is_supported("file://test"));
assert!(is_supported("test.svg"));
}
}

10
crates/epaint/src/image.rs

@ -1,4 +1,5 @@
use crate::{textures::TextureOptions, Color32}; use crate::{textures::TextureOptions, Color32};
use std::sync::Arc;
/// An image stored in RAM. /// An image stored in RAM.
/// ///
@ -11,7 +12,7 @@ use crate::{textures::TextureOptions, Color32};
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ImageData { pub enum ImageData {
/// RGBA image. /// RGBA image.
Color(ColorImage), Color(Arc<ColorImage>),
/// Used for the font texture. /// Used for the font texture.
Font(FontImage), Font(FontImage),
@ -226,6 +227,13 @@ impl std::ops::IndexMut<(usize, usize)> for ColorImage {
impl From<ColorImage> for ImageData { impl From<ColorImage> for ImageData {
#[inline(always)] #[inline(always)]
fn from(image: ColorImage) -> Self { fn from(image: ColorImage) -> Self {
Self::Color(Arc::new(image))
}
}
impl From<Arc<ColorImage>> for ImageData {
#[inline]
fn from(image: Arc<ColorImage>) -> Self {
Self::Color(image) Self::Color(image)
} }
} }

5
crates/epaint/src/texture_handle.rs

@ -95,6 +95,11 @@ impl TextureHandle {
crate::Vec2::new(w as f32, h as f32) crate::Vec2::new(w as f32, h as f32)
} }
/// `width x height x bytes_per_pixel`
pub fn byte_size(&self) -> usize {
self.tex_mngr.read().meta(self.id).unwrap().bytes_used()
}
/// width / height /// width / height
pub fn aspect_ratio(&self) -> f32 { pub fn aspect_ratio(&self) -> f32 {
let [w, h] = self.size(); let [w, h] = self.size();

8
examples/download_image/Cargo.toml

@ -12,8 +12,10 @@ 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 = ["image"] } egui_extras = { path = "../../crates/egui_extras", features = [
ehttp = "0.2" "http",
"image",
"log",
] }
env_logger = "0.10" env_logger = "0.10"
image = { version = "0.24", default-features = false, features = ["jpeg"] } image = { version = "0.24", default-features = false, features = ["jpeg"] }
poll-promise = "0.2"

63
examples/download_image/src/main.rs

@ -1,8 +1,6 @@
#![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 egui_extras::RetainedImage;
use poll_promise::Promise;
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`).
@ -10,55 +8,34 @@ fn main() -> Result<(), eframe::Error> {
eframe::run_native( eframe::run_native(
"Download and show an image with eframe/egui", "Download and show an image with eframe/egui",
options, options,
Box::new(|_cc| Box::<MyApp>::default()), Box::new(|cc| {
// Without the following call, the `Image2` created below
// will simply output `not supported` error messages.
egui_extras::loaders::install(&cc.egui_ctx);
Box::new(MyApp)
}),
) )
} }
#[derive(Default)] #[derive(Default)]
struct MyApp { struct MyApp;
/// `None` when download hasn't started yet.
promise: Option<Promise<ehttp::Result<RetainedImage>>>,
}
impl eframe::App for MyApp { 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) {
let promise = self.promise.get_or_insert_with(|| { egui::CentralPanel::default().show(ctx, |ui| {
// Begin download. let width = ui.available_width();
// We download the image using `ehttp`, a library that works both in WASM and on native. let half_height = ui.available_height() / 2.0;
// We use the `poll-promise` library to communicate with the UI thread.
let ctx = ctx.clone();
let (sender, promise) = Promise::new();
let request = ehttp::Request::get("https://picsum.photos/seed/1.759706314/1024");
ehttp::fetch(request, move |response| {
let image = response.and_then(parse_response);
sender.send(image); // send the results back to the UI thread.
ctx.request_repaint(); // wake up UI thread
});
promise
});
egui::CentralPanel::default().show(ctx, |ui| match promise.ready() { ui.allocate_ui(egui::Vec2::new(width, half_height), |ui| {
None => { ui.add(egui::Image2::from_uri(
ui.spinner(); // still loading "https://picsum.photos/seed/1.759706314/1024",
} ))
Some(Err(err)) => { });
ui.colored_label(ui.visuals().error_fg_color, err); // something went wrong ui.allocate_ui(egui::Vec2::new(width, half_height), |ui| {
} ui.add(egui::Image2::from_uri(
Some(Ok(image)) => { "https://this-is-hopefully-not-a-real-website.rs/image.png",
image.show_max_size(ui, ui.available_size()); ))
} });
}); });
} }
} }
#[allow(clippy::needless_pass_by_value)]
fn parse_response(response: ehttp::Response) -> Result<RetainedImage, String> {
let content_type = response.content_type().unwrap_or_default();
if content_type.starts_with("image/") {
RetainedImage::from_image_bytes(&response.url, &response.bytes)
} else {
Err(format!(
"Expected image, found content-type {content_type:?}"
))
}
}

2
examples/retained_image/Cargo.toml

@ -12,6 +12,6 @@ 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 = ["image"] } egui_extras = { path = "../../crates/egui_extras", features = ["image", "log"] }
env_logger = "0.10" env_logger = "0.10"
image = { version = "0.24", default-features = false, features = ["png"] } image = { version = "0.24", default-features = false, features = ["png"] }

1
examples/screenshot/Cargo.toml

@ -17,5 +17,4 @@ eframe = { path = "../../crates/eframe", features = [
"wgpu", "wgpu",
] } ] }
env_logger = "0.10" env_logger = "0.10"
itertools = "0.10.3"
image = { version = "0.24", default-features = false, features = ["png"] } image = { version = "0.24", default-features = false, features = ["png"] }

2
examples/svg/Cargo.toml

@ -12,5 +12,5 @@ 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 = ["svg"] } egui_extras = { path = "../../crates/egui_extras", features = ["log", "svg"] }
env_logger = "0.10" env_logger = "0.10"

32
examples/svg/src/main.rs

@ -15,26 +15,16 @@ fn main() -> Result<(), eframe::Error> {
eframe::run_native( eframe::run_native(
"svg example", "svg example",
options, options,
Box::new(|_cc| Box::<MyApp>::default()), Box::new(|cc| {
// Without the following call, the `Image2` created below
// will simply output a `not supported` error message.
egui_extras::loaders::install(&cc.egui_ctx);
Box::new(MyApp)
}),
) )
} }
struct MyApp { struct MyApp;
svg_image: egui_extras::RetainedImage,
}
impl Default for MyApp {
fn default() -> Self {
Self {
svg_image: egui_extras::RetainedImage::from_svg_bytes_with_size(
"rustacean-flat-happy.svg",
include_bytes!("rustacean-flat-happy.svg"),
egui_extras::image::FitTo::Original,
)
.unwrap(),
}
}
}
impl eframe::App for MyApp { 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) {
@ -45,7 +35,13 @@ impl eframe::App for MyApp {
ui.separator(); ui.separator();
let max_size = ui.available_size(); let max_size = ui.available_size();
self.svg_image.show_size(ui, max_size); ui.add(
egui::Image2::from_static_bytes(
"ferris.svg",
include_bytes!("rustacean-flat-happy.svg"),
)
.size_hint(max_size),
);
}); });
} }
} }

Loading…
Cancel
Save