mirror of https://github.com/emilk/egui.git
Browse Source
* 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
24 changed files with 1343 additions and 79 deletions
@ -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, |
|||
} |
|||
} |
|||
} |
@ -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; |
@ -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() |
|||
} |
|||
} |
@ -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() |
|||
} |
|||
} |
@ -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")); |
|||
} |
|||
} |
@ -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")); |
|||
} |
|||
} |
Loading…
Reference in new issue