Skip to content

Commit

Permalink
Final polish for new image loading (#3328)
Browse files Browse the repository at this point in the history
* add egui logo to widget gallery

* improve "no image loaders" error message

* rework static URIs to accept `Cow<'static>`

* remove `RetainedImage` from `http_app` in `egui_demo_app`

* hide `RetainedImage` from docs

* use `ui.image`/`Image` over `RawImage`

* remove last remanant of `RawImage`

* remove unused doc link

* add style option to disable image spinners

* use `Into<Image>` instead of `Into<ImageSource>` to allow configuring the underlying image

* propagate `image_options` through `ImageButton`

* calculate image size properly in `Button`

* properly calculate size in `ImageButton`

* Update crates/egui/src/widgets/image.rs

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

* improve no image loaders error message

* add `size()` helper to `TexturePoll`

* try get size from poll in `Button`

* add `paint_at` to `Spinner`

* use `Spinner::paint_at` and hover on image button response

* `show_spinner` -> `show_loading_spinner`

* avoid `allocate_ui` in `Image` when painting spinner

* make icon smaller + remove old texture

* add `load_and_calculate_size` + expose `paint_image_at`

* update `egui_plot` to paint image in the right place

* Add helpers for painting an ImageSource directly

* Use max_size=INF as default

* Use new API in WidgetGallery

* Make egui_demo_app work by default

* Remove Option from scale

* Refactor ImageSize

* Fix docstring

* Small refactor

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
  • Loading branch information
jprochazk and emilk authored Sep 13, 2023
1 parent fc3bddd commit 67a3fca
Show file tree
Hide file tree
Showing 25 changed files with 534 additions and 501 deletions.
5 changes: 2 additions & 3 deletions crates/egui-wgpu/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -605,9 +605,8 @@ impl Renderer {

/// Get the WGPU texture and bind group associated to a texture that has been allocated by egui.
///
/// This could be used by custom paint hooks to render images that have been added through with
/// [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html)
/// or [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture).
/// This could be used by custom paint hooks to render images that have been added through
/// [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture).
pub fn texture(
&self,
id: &epaint::TextureId,
Expand Down
21 changes: 15 additions & 6 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![warn(missing_docs)] // Let's keep `Context` well-documented.

use std::borrow::Cow;
use std::sync::Arc;

use crate::load::Bytes;
Expand Down Expand Up @@ -1145,7 +1146,7 @@ impl Context {
/// });
///
/// // Show the image:
/// ui.raw_image((texture.id(), texture.size_vec2()));
/// ui.image((texture.id(), texture.size_vec2()));
/// }
/// }
/// ```
Expand Down Expand Up @@ -1691,14 +1692,14 @@ impl Context {
let mut size = vec2(w as f32, h as f32);
size *= (max_preview_size.x / size.x).min(1.0);
size *= (max_preview_size.y / size.y).min(1.0);
ui.raw_image(SizedTexture::new(texture_id, size))
ui.image(SizedTexture::new(texture_id, size))
.on_hover_ui(|ui| {
// show larger on hover
let max_size = 0.5 * ui.ctx().screen_rect().size();
let mut size = vec2(w as f32, h as f32);
size *= max_size.x / size.x.max(max_size.x);
size *= max_size.y / size.y.max(max_size.y);
ui.raw_image(SizedTexture::new(texture_id, size));
ui.image(SizedTexture::new(texture_id, size));
});

ui.label(format!("{w} x {h}"));
Expand Down Expand Up @@ -1911,8 +1912,8 @@ impl Context {
/// Associate some static bytes with a `uri`.
///
/// The same `uri` may be passed to [`Ui::image`] later to load the bytes as an image.
pub fn include_bytes(&self, uri: &'static str, bytes: impl Into<Bytes>) {
self.loaders().include.insert(uri, bytes.into());
pub fn include_bytes(&self, uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) {
self.loaders().include.insert(uri, bytes);
}

/// Returns `true` if the chain of bytes, image, or texture loaders
Expand Down Expand Up @@ -2038,17 +2039,25 @@ impl Context {
///
/// # Errors
/// This may fail with:
/// - [`LoadError::NoImageLoaders`][no_image_loaders] if tbere are no registered image loaders.
/// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`.
/// - [`LoadError::Custom`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed.
///
/// ⚠ May deadlock if called from within an `ImageLoader`!
///
/// [no_image_loaders]: crate::load::LoadError::NoImageLoaders
/// [not_supported]: crate::load::LoadError::NotSupported
/// [custom]: crate::load::LoadError::Custom
pub fn try_load_image(&self, uri: &str, size_hint: load::SizeHint) -> load::ImageLoadResult {
crate::profile_function!();

for loader in self.loaders().image.lock().iter() {
let loaders = self.loaders();
let loaders = loaders.image.lock();
if loaders.is_empty() {
return Err(load::LoadError::NoImageLoaders);
}

for loader in loaders.iter() {
match loader.load(self, uri, size_hint) {
Err(load::LoadError::NotSupported) => continue,
result => return result,
Expand Down
7 changes: 5 additions & 2 deletions crates/egui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
//! ui.separator();
//!
//! # let my_image = egui::TextureId::default();
//! ui.raw_image((my_image, egui::Vec2::new(640.0, 480.0)));
//! ui.image((my_image, egui::Vec2::new(640.0, 480.0)));
//!
//! ui.collapsing("Click to see what is hidden!", |ui| {
//! ui.label("Not much, as it turns out");
Expand Down Expand Up @@ -442,7 +442,10 @@ pub fn warn_if_debug_build(ui: &mut crate::Ui) {
#[macro_export]
macro_rules! include_image {
($path: literal) => {
$crate::ImageSource::Bytes($path, $crate::load::Bytes::Static(include_bytes!($path)))
$crate::ImageSource::Bytes(
::std::borrow::Cow::Borrowed($path),
$crate::load::Bytes::Static(include_bytes!($path)),
)
};
}

Expand Down
126 changes: 30 additions & 96 deletions crates/egui/src/load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,29 @@
//! For example, a loader may determine that it doesn't support loading a specific URI
//! if the protocol does not match what it expects.
mod bytes_loader;
mod texture_loader;

use self::bytes_loader::DefaultBytesLoader;
use self::texture_loader::DefaultTextureLoader;
use crate::Context;
use ahash::HashMap;
use epaint::mutex::Mutex;
use epaint::util::FloatOrd;
use epaint::util::OrderedFloat;
use epaint::TextureHandle;
use epaint::{textures::TextureOptions, ColorImage, TextureId, Vec2};
use std::borrow::Cow;
use std::fmt::Debug;
use std::ops::Deref;
use std::{error::Error as StdError, fmt::Display, sync::Arc};

/// Represents a failed attempt at loading an image.
#[derive(Clone, Debug)]
pub enum LoadError {
/// There are no image loaders installed.
NoImageLoaders,

/// This loader does not support this protocol or image format.
NotSupported,

Expand All @@ -76,6 +85,9 @@ pub enum LoadError {
impl Display for LoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LoadError::NoImageLoaders => f.write_str(
"No image loaders are installed. If you're trying to load some images \
for the first time, follow the steps outlined in https://docs.rs/egui/latest/egui/load/index.html"),
LoadError::NotSupported => f.write_str("not supported"),
LoadError::Custom(message) => f.write_str(message),
}
Expand Down Expand Up @@ -342,7 +354,7 @@ pub trait ImageLoader {
}

/// A texture with a known size.
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SizedTexture {
pub id: TextureId,
pub size: Vec2,
Expand Down Expand Up @@ -370,7 +382,13 @@ impl SizedTexture {
impl From<(TextureId, Vec2)> for SizedTexture {
#[inline]
fn from((id, size): (TextureId, Vec2)) -> Self {
SizedTexture { id, size }
Self { id, size }
}
}

impl<'a> From<&'a TextureHandle> for SizedTexture {
fn from(handle: &'a TextureHandle) -> Self {
Self::from_handle(handle)
}
}

Expand All @@ -379,7 +397,7 @@ impl From<(TextureId, Vec2)> for SizedTexture {
/// This is similar to [`std::task::Poll`], but the `Pending` variant
/// contains an optional `size`, which may be used during layout to
/// pre-allocate space the image.
#[derive(Clone)]
#[derive(Clone, Copy)]
pub enum TexturePoll {
/// Texture is loading.
Pending {
Expand All @@ -391,6 +409,15 @@ pub enum TexturePoll {
Ready { texture: SizedTexture },
}

impl TexturePoll {
pub fn size(self) -> Option<Vec2> {
match self {
TexturePoll::Pending { size } => size,
TexturePoll::Ready { texture } => Some(texture.size),
}
}
}

pub type TextureLoadResult = Result<TexturePoll>;

/// Represents a loader capable of loading a full texture.
Expand Down Expand Up @@ -447,99 +474,6 @@ pub trait TextureLoader {
fn byte_size(&self) -> usize;
}

#[derive(Default)]
pub(crate) struct DefaultBytesLoader {
cache: Mutex<HashMap<&'static str, Bytes>>,
}

impl DefaultBytesLoader {
pub(crate) fn insert(&self, uri: &'static str, bytes: impl Into<Bytes>) {
self.cache.lock().entry(uri).or_insert_with(|| bytes.into());
}
}

impl BytesLoader for DefaultBytesLoader {
fn id(&self) -> &str {
generate_loader_id!(DefaultBytesLoader)
}

fn load(&self, _: &Context, uri: &str) -> BytesLoadResult {
match self.cache.lock().get(uri).cloned() {
Some(bytes) => Ok(BytesPoll::Ready {
size: None,
bytes,
mime: None,
}),
None => Err(LoadError::NotSupported),
}
}

fn forget(&self, uri: &str) {
let _ = self.cache.lock().remove(uri);
}

fn forget_all(&self) {
self.cache.lock().clear();
}

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 id(&self) -> &str {
generate_loader_id!(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 forget_all(&self) {
self.cache.lock().clear();
}

fn end_frame(&self, _: usize) {}

fn byte_size(&self) -> usize {
self.cache
.lock()
.values()
.map(|texture| texture.byte_size())
.sum()
}
}

type BytesLoaderImpl = Arc<dyn BytesLoader + Send + Sync + 'static>;
type ImageLoaderImpl = Arc<dyn ImageLoader + Send + Sync + 'static>;
type TextureLoaderImpl = Arc<dyn TextureLoader + Send + Sync + 'static>;
Expand Down
57 changes: 57 additions & 0 deletions crates/egui/src/load/bytes_loader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use super::*;

#[derive(Default)]
pub struct DefaultBytesLoader {
cache: Mutex<HashMap<Cow<'static, str>, Bytes>>,
}

impl DefaultBytesLoader {
pub fn insert(&self, uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) {
self.cache
.lock()
.entry(uri.into())
.or_insert_with_key(|uri| {
let bytes: Bytes = bytes.into();

#[cfg(feature = "log")]
log::trace!("loaded {} bytes for uri {uri:?}", bytes.len());

bytes
});
}
}

impl BytesLoader for DefaultBytesLoader {
fn id(&self) -> &str {
generate_loader_id!(DefaultBytesLoader)
}

fn load(&self, _: &Context, uri: &str) -> BytesLoadResult {
match self.cache.lock().get(uri).cloned() {
Some(bytes) => Ok(BytesPoll::Ready {
size: None,
bytes,
mime: None,
}),
None => Err(LoadError::NotSupported),
}
}

fn forget(&self, uri: &str) {
#[cfg(feature = "log")]
log::trace!("forget {uri:?}");

let _ = self.cache.lock().remove(uri);
}

fn forget_all(&self) {
#[cfg(feature = "log")]
log::trace!("forget all");

self.cache.lock().clear();
}

fn byte_size(&self) -> usize {
self.cache.lock().values().map(|bytes| bytes.len()).sum()
}
}
Loading

0 comments on commit 67a3fca

Please sign in to comment.