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