Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Final polish for new image loading #3328

Merged
merged 33 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e6c8e61
add egui logo to widget gallery
jprochazk Sep 12, 2023
1686b59
improve "no image loaders" error message
jprochazk Sep 12, 2023
344c172
rework static URIs to accept `Cow<'static>`
jprochazk Sep 12, 2023
fb9f298
remove `RetainedImage` from `http_app` in `egui_demo_app`
jprochazk Sep 12, 2023
6e82c73
hide `RetainedImage` from docs
jprochazk Sep 12, 2023
cb5e3c4
use `ui.image`/`Image` over `RawImage`
jprochazk Sep 13, 2023
63d6d31
remove last remanant of `RawImage`
jprochazk Sep 13, 2023
361626e
Merge branch 'master' into improved-texture-loading-p3
jprochazk Sep 13, 2023
b955a4f
remove unused doc link
jprochazk Sep 13, 2023
6589fdf
add style option to disable image spinners
jprochazk Sep 13, 2023
cd64d2e
use `Into<Image>` instead of `Into<ImageSource>` to allow configuring…
jprochazk Sep 13, 2023
d40834e
propagate `image_options` through `ImageButton`
jprochazk Sep 13, 2023
6d09e81
calculate image size properly in `Button`
jprochazk Sep 13, 2023
c273826
properly calculate size in `ImageButton`
jprochazk Sep 13, 2023
238584c
Update crates/egui/src/widgets/image.rs
jprochazk Sep 13, 2023
a6774f7
improve no image loaders error message
jprochazk Sep 13, 2023
9071a65
add `size()` helper to `TexturePoll`
jprochazk Sep 13, 2023
8e5e1e0
try get size from poll in `Button`
jprochazk Sep 13, 2023
dfe266b
add `paint_at` to `Spinner`
jprochazk Sep 13, 2023
35576c0
use `Spinner::paint_at` and hover on image button response
jprochazk Sep 13, 2023
4daa256
`show_spinner` -> `show_loading_spinner`
jprochazk Sep 13, 2023
87f2411
avoid `allocate_ui` in `Image` when painting spinner
jprochazk Sep 13, 2023
c2cd018
make icon smaller + remove old texture
jprochazk Sep 13, 2023
d2056b0
add `load_and_calculate_size` + expose `paint_image_at`
jprochazk Sep 13, 2023
5ee02a4
update `egui_plot` to paint image in the right place
jprochazk Sep 13, 2023
c6c0f27
Add helpers for painting an ImageSource directly
emilk Sep 13, 2023
f6df497
Use max_size=INF as default
emilk Sep 13, 2023
9af8b77
Use new API in WidgetGallery
emilk Sep 13, 2023
5cdbeff
Make egui_demo_app work by default
emilk Sep 13, 2023
e6fae9a
Remove Option from scale
emilk Sep 13, 2023
a69092b
Refactor ImageSize
emilk Sep 13, 2023
682d405
Fix docstring
emilk Sep 13, 2023
9d3ba1b
Small refactor
emilk Sep 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading