Improved texture loading (#3315)

* rework loading around `Arc<Loaders>`

* use `Bytes` instead of splitting api

* remove unwraps in `texture_handle`

* make `FileLoader` optional under `file` feature

* hide http load error stack trace from UI

* implement image fit

* support more image sources

* center spinner if we know size ahead of time

* allocate final size for spinner

* improve image format guessing

* remove `ui.image`, `Image`, add `RawImage`

* deprecate `RetainedImage`

* `image2` -> `image`

* add viewer example

* update `examples/image` + remove `svg` and `download_image` exapmles

* fix lints and tests

* fix doc link

* add image controls to `images` example

* add more `From` str-like types

* add api to forget all images

* fix max size

* do not scale original size unless necessary

* fix doc link

* add more docs for `Image` and `RawImage`

* make paint_at `pub`

* update `ImageButton` to use new `Image` API

* fix double rendering

* `SizeHint::Original` -> `Scale` + remove `Option` wrapper

* Update crates/egui/src/load.rs

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

* remove special `None` value for `forget`

* Update crates/egui/src/load.rs

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

* add more examples to `ui.image` + add `include_image` macro

* Update crates/egui/src/ui.rs

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

* update `menu_image_button` to use `ImageSource`

* `OrderedFloat::get` -> `into_inner`

* derive `Eq` on `SizedTexture`

* add `id` to loaders + `is_installed` check

* move `images` to demo + simplify `images` example

* log trace when installing loaders

* fix lint

* fix doc link

* add more documentation

* more `egui_extras::loaders` docs

* Update examples/images/src/main.rs

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

* update `images` example screenshots + readme

* remove unused `rfd` from `images` example

* Update crates/egui_extras/src/loaders/ehttp_loader.rs

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

* add `must_use` on `Image` and `RawImage`

* document `loaders::install` multiple call safety

* Update crates/egui_extras/Cargo.toml

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

* reshuffle `is_loader_installed`

* make `include_image` produce `ImageSource` + update docs

* update `include_image` docs

* remove `None` mentions from loader `forget`

* inline `From` texture id + size for `SizedTexture`

* add warning about statically known path

* change image load error + use in image button

* add `.size()` to `Image`

* Update crates/egui_demo_app/Cargo.toml

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

* add explanations to image viewer ui

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
Jan Procházka 2023-09-12 10:39:17 +02:00 committed by GitHub
parent dbcf15b49e
commit 2bc6814acc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1563 additions and 770 deletions

66
Cargo.lock generated
View File

@ -1083,16 +1083,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
[[package]]
name = "download_image"
version = "0.1.0"
dependencies = [
"eframe",
"egui_extras",
"env_logger",
"image",
]
[[package]]
name = "dyn-clonable"
version = "0.9.0"
@ -1231,6 +1221,7 @@ dependencies = [
"image",
"log",
"poll-promise",
"rfd",
"serde",
"wasm-bindgen",
"wasm-bindgen-futures",
@ -1264,6 +1255,7 @@ dependencies = [
"ehttp",
"image",
"log",
"mime_guess",
"puffin",
"resvg",
"serde",
@ -2026,6 +2018,16 @@ dependencies = [
"png",
]
[[package]]
name = "images"
version = "0.1.0"
dependencies = [
"eframe",
"egui_extras",
"env_logger",
"image",
]
[[package]]
name = "imagesize"
version = "0.10.1"
@ -2310,6 +2312,22 @@ dependencies = [
"paste",
]
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -2998,16 +3016,6 @@ dependencies = [
"usvg",
]
[[package]]
name = "retained_image"
version = "0.1.0"
dependencies = [
"eframe",
"egui_extras",
"env_logger",
"image",
]
[[package]]
name = "rfd"
version = "0.11.4"
@ -3408,15 +3416,6 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "svg"
version = "0.1.0"
dependencies = [
"eframe",
"egui_extras",
"env_logger",
]
[[package]]
name = "svgtypes"
version = "0.8.2"
@ -3747,6 +3746,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "unicase"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-bidi"
version = "0.3.13"

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -2,9 +2,11 @@
use std::sync::Arc;
use crate::load::Bytes;
use crate::load::SizedTexture;
use crate::{
animation_manager::AnimationManager, data::output::PlatformOutput, frame_state::FrameState,
input_state::*, layers::GraphicLayers, memory::Options, os::OperatingSystem,
input_state::*, layers::GraphicLayers, load::Loaders, memory::Options, os::OperatingSystem,
output::FullOutput, util::IdTypeMap, TextureHandle, *,
};
use epaint::{mutex::*, stats::*, text::Fonts, TessellationOptions, *};
@ -167,7 +169,7 @@ struct ContextImpl {
#[cfg(feature = "accesskit")]
accesskit_node_classes: accesskit::NodeClassSet,
loaders: load::Loaders,
loaders: Arc<Loaders>,
}
impl ContextImpl {
@ -1143,7 +1145,7 @@ impl Context {
/// });
///
/// // Show the image:
/// ui.image(texture, texture.size_vec2());
/// ui.raw_image((texture.id(), texture.size_vec2()));
/// }
/// }
/// ```
@ -1689,14 +1691,15 @@ 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.image(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.image(texture_id, size);
});
ui.raw_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.label(format!("{w} x {h}"));
ui.label(format!("{:.3} MB", meta.bytes_used() as f64 * 1e-6));
@ -1907,60 +1910,90 @@ impl Context {
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));
/// 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());
}
/// 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));
/// Returns `true` if the chain of bytes, image, or texture loaders
/// contains a loader with the given `id`.
pub fn is_loader_installed(&self, id: &str) -> bool {
let loaders = self.loaders();
let in_bytes = loaders.bytes.lock().iter().any(|loader| loader.id() == id);
let in_image = loaders.image.lock().iter().any(|loader| loader.id() == id);
let in_texture = loaders
.texture
.lock()
.iter()
.any(|loader| loader.id() == id);
in_bytes || in_image || in_texture
}
/// 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));
self.loaders().bytes.lock().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));
self.loaders().image.lock().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));
self.loaders().texture.lock().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 _;
use load::BytesLoader as _;
ctx.loaders.include.forget(uri);
crate::profile_function!();
for loader in &ctx.loaders.bytes {
loader.forget(uri);
}
let loaders = self.loaders();
for loader in &ctx.loaders.image {
loader.forget(uri);
}
loaders.include.forget(uri);
for loader in loaders.bytes.lock().iter() {
loader.forget(uri);
}
for loader in loaders.image.lock().iter() {
loader.forget(uri);
}
for loader in loaders.texture.lock().iter() {
loader.forget(uri);
}
}
for loader in &ctx.loaders.texture {
loader.forget(uri);
}
});
/// Release all memory and textures related to images used in [`Ui::image`] or [`Image`].
///
/// If you attempt to load any images again, they will be reloaded from scratch.
pub fn forget_all_images(&self) {
use load::BytesLoader as _;
crate::profile_function!();
let loaders = self.loaders();
loaders.include.forget_all();
for loader in loaders.bytes.lock().iter() {
loader.forget_all();
}
for loader in loaders.image.lock().iter() {
loader.forget_all();
}
for loader in loaders.texture.lock().iter() {
loader.forget_all();
}
}
/// Try loading the bytes from the given uri using any available bytes loaders.
@ -1977,11 +2010,14 @@ impl Context {
/// - [`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 a `BytesLoader`!
///
/// [not_supported]: crate::load::LoadError::NotSupported
/// [custom]: crate::load::LoadError::Custom
pub fn try_load_bytes(&self, uri: &str) -> load::BytesLoadResult {
let loaders = self.loaders();
for loader in &loaders.bytes {
crate::profile_function!();
for loader in self.loaders().bytes.lock().iter() {
match loader.load(self, uri) {
Err(load::LoadError::NotSupported) => continue,
result => return result,
@ -2005,11 +2041,14 @@ impl Context {
/// - [`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`!
///
/// [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 {
let loaders = self.loaders();
for loader in &loaders.image {
crate::profile_function!();
for loader in self.loaders().image.lock().iter() {
match loader.load(self, uri, size_hint) {
Err(load::LoadError::NotSupported) => continue,
result => return result,
@ -2033,6 +2072,8 @@ impl Context {
/// - [`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 a `TextureLoader`!
///
/// [not_supported]: crate::load::LoadError::NotSupported
/// [custom]: crate::load::LoadError::Custom
pub fn try_load_texture(
@ -2041,9 +2082,9 @@ impl Context {
texture_options: TextureOptions,
size_hint: load::SizeHint,
) -> load::TextureLoadResult {
let loaders = self.loaders();
crate::profile_function!();
for loader in &loaders.texture {
for loader in self.loaders().texture.lock().iter() {
match loader.load(self, uri, texture_options, size_hint) {
Err(load::LoadError::NotSupported) => continue,
result => return result,
@ -2053,9 +2094,9 @@ impl Context {
Err(load::LoadError::NotSupported)
}
fn loaders(&self) -> load::Loaders {
fn loaders(&self) -> Arc<Loaders> {
crate::profile_function!();
self.read(|this| this.loaders.clone()) // TODO(emilk): something less slow
self.read(|this| this.loaders.clone())
}
}

View File

@ -84,7 +84,7 @@
//! ui.separator();
//!
//! # let my_image = egui::TextureId::default();
//! ui.image(my_image, [640.0, 480.0]);
//! ui.raw_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");
@ -424,6 +424,28 @@ pub fn warn_if_debug_build(ui: &mut crate::Ui) {
// ----------------------------------------------------------------------------
/// Include an image in the binary.
///
/// This is a wrapper over `include_bytes!`, and behaves in the same way.
///
/// It produces an [`ImageSource`] which can be used directly in [`Ui::image`] or [`Image::new`]:
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.image(egui::include_image!("../assets/ferris.png"));
/// ui.add(
/// egui::Image::new(egui::include_image!("../assets/ferris.png"))
/// .rounding(egui::Rounding::same(6.0))
/// );
/// # });
/// ```
#[macro_export]
macro_rules! include_image {
($path: literal) => {
$crate::ImageSource::Bytes($path, $crate::load::Bytes::Static(include_bytes!($path)))
};
}
/// Create a [`Hyperlink`](crate::Hyperlink) to the current [`file!()`] (and line) on Github
///
/// ```

View File

@ -1,8 +1,12 @@
//! Types and traits related to image loading.
//! # 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).
//! If you just want to display some images, [`egui_extras`](https://crates.io/crates/egui_extras/)
//! will get you up and running quickly with its reasonable default implementations of the traits described below.
//!
//! 1. Add [`egui_extras`](https://crates.io/crates/egui_extras/) as a dependency with the `all-loaders` feature.
//! 2. Add a call to [`egui_extras::loaders::install`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html)
//! in your app's setup code.
//! 3. Use [`Ui::image`][`crate::ui::Ui::image`] with some [`ImageSource`][`crate::ImageSource`].
//!
//! ## Loading process
//!
@ -14,13 +18,13 @@
//! 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", ...)
//! ui.image("file://image.png")
//! └► Context::try_load_texture
//! └► TextureLoader::load
//! └► Context::try_load_image
//! └► ImageLoader::load
//! └► Context::try_load_bytes
//! └► BytesLoader::load
//! ```
//!
//! As each layer attempts to load the URI, it first asks the layer below it
@ -51,11 +55,15 @@
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::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 {
/// This loader does not support this protocol or image format.
@ -85,11 +93,10 @@ pub type Result<T, E = LoadError> = std::result::Result<T, E>;
/// All variants will preserve the original aspect ratio.
///
/// Similar to `usvg::FitTo`.
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SizeHint {
/// Keep original size.
#[default]
Original,
/// Scale original size by some factor.
Scale(OrderedFloat<f32>),
/// Scale to width.
Width(u32),
@ -101,22 +108,36 @@ pub enum SizeHint {
Size(u32, u32),
}
impl Default for SizeHint {
fn default() -> Self {
Self::Scale(1.0.ord())
}
}
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];
/// Represents a byte buffer.
///
/// This is essentially `Cow<'static, [u8]>` but with the `Owned` variant being an `Arc`.
#[derive(Clone)]
pub enum Bytes {
Static(&'static [u8]),
Shared(Arc<[u8]>),
}
impl Debug for Bytes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Static(arg0) => f.debug_tuple("Static").field(&arg0.len()).finish(),
Self::Shared(arg0) => f.debug_tuple("Shared").field(&arg0.len()).finish(),
}
}
}
impl From<&'static [u8]> for Bytes {
#[inline]
fn from(value: &'static [u8]) -> Self {
@ -124,6 +145,13 @@ impl From<&'static [u8]> for Bytes {
}
}
impl<const N: usize> From<&'static [u8; N]> for Bytes {
#[inline]
fn from(value: &'static [u8; N]) -> Self {
Bytes::Static(value)
}
}
impl From<Arc<[u8]>> for Bytes {
#[inline]
fn from(value: Arc<[u8]>) -> Self {
@ -131,6 +159,13 @@ impl From<Arc<[u8]>> for Bytes {
}
}
impl From<Vec<u8>> for Bytes {
#[inline]
fn from(value: Vec<u8>) -> Self {
Bytes::Shared(value.into())
}
}
impl AsRef<[u8]> for Bytes {
#[inline]
fn as_ref(&self) -> &[u8] {
@ -150,27 +185,59 @@ impl Deref for Bytes {
}
}
/// Represents bytes which are currently being loaded.
///
/// 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)]
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>,
size: Option<Vec2>,
},
/// Bytes are loaded.
Ready {
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Size>,
size: Option<Vec2>,
/// File contents, e.g. the contents of a `.png`.
bytes: Bytes,
/// Mime type of the content, e.g. `image/png`.
///
/// Set if known (e.g. from `Content-Type` HTTP header).
mime: Option<String>,
},
}
/// Used to get a unique ID when implementing one of the loader traits: [`BytesLoader::id`], [`ImageLoader::id`], and [`TextureLoader::id`].
///
/// This just expands to `module_path!()` concatenated with the given type name.
#[macro_export]
macro_rules! generate_loader_id {
($ty:ident) => {
concat!(module_path!(), "::", stringify!($ty))
};
}
pub use crate::generate_loader_id;
pub type BytesLoadResult = Result<BytesPoll>;
/// Represents a loader capable of loading raw unstructured bytes.
///
/// It should also provide any subsequent loaders a hint for what the bytes may
/// represent using [`BytesPoll::Ready::mime`], if it can be inferred.
///
/// Implementations are expected to cache at least each `URI`.
pub trait BytesLoader {
/// Unique ID of this loader.
///
/// To reduce the chance of collisions, use [`generate_loader_id`] for this.
fn id(&self) -> &str;
/// Try loading the bytes from the given uri.
///
/// Implementations should call `ctx.request_repaint` to wake up the ui
@ -191,6 +258,12 @@ pub trait BytesLoader {
/// so that it may be fully reloaded.
fn forget(&self, uri: &str);
/// Forget all URIs ever given to this loader.
///
/// If the loader caches any URIs, the entire cache should be cleared,
/// so that all of them may be fully reloaded.
fn forget_all(&self);
/// 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) {
@ -201,12 +274,17 @@ pub trait BytesLoader {
fn byte_size(&self) -> usize;
}
/// Represents an image which is currently being loaded.
///
/// 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)]
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>,
size: Option<Vec2>,
},
/// Image is loaded.
@ -215,7 +293,18 @@ pub enum ImagePoll {
pub type ImageLoadResult = Result<ImagePoll>;
/// Represents a loader capable of loading a raw image.
///
/// Implementations are expected to cache at least each `URI`.
pub trait ImageLoader {
/// Unique ID of this loader.
///
/// To reduce the chance of collisions, include `module_path!()` as part of this ID.
///
/// For example: `concat!(module_path!(), "::MyLoader")`
/// for `my_crate::my_loader::MyLoader`.
fn id(&self) -> &str;
/// Try loading the image from the given uri.
///
/// Implementations should call `ctx.request_repaint` to wake up the ui
@ -236,6 +325,12 @@ pub trait ImageLoader {
/// so that it may be fully reloaded.
fn forget(&self, uri: &str);
/// Forget all URIs ever given to this loader.
///
/// If the loader caches any URIs, the entire cache should be cleared,
/// so that all of them may be fully reloaded.
fn forget_all(&self);
/// 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) {
@ -247,27 +342,49 @@ pub trait ImageLoader {
}
/// A texture with a known size.
#[derive(Clone)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SizedTexture {
pub id: TextureId,
pub size: Size,
pub size: Vec2,
}
impl SizedTexture {
/// Create a [`SizedTexture`] from a texture `id` with a specific `size`.
pub fn new(id: impl Into<TextureId>, size: impl Into<Vec2>) -> Self {
Self {
id: id.into(),
size: size.into(),
}
}
/// Fetch the [id][`SizedTexture::id`] and [size][`SizedTexture::size`] from a [`TextureHandle`].
pub fn from_handle(handle: &TextureHandle) -> Self {
let size = handle.size();
Self {
id: handle.id(),
size: handle.size(),
size: Vec2::new(size[0] as f32, size[1] as f32),
}
}
}
impl From<(TextureId, Vec2)> for SizedTexture {
#[inline]
fn from((id, size): (TextureId, Vec2)) -> Self {
SizedTexture { id, size }
}
}
/// Represents a texture is currently being loaded.
///
/// 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)]
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>,
size: Option<Vec2>,
},
/// Texture is loaded.
@ -276,7 +393,18 @@ pub enum TexturePoll {
pub type TextureLoadResult = Result<TexturePoll>;
/// Represents a loader capable of loading a full texture.
///
/// Implementations are expected to cache each combination of `(URI, TextureOptions)`.
pub trait TextureLoader {
/// Unique ID of this loader.
///
/// To reduce the chance of collisions, include `module_path!()` as part of this ID.
///
/// For example: `concat!(module_path!(), "::MyLoader")`
/// for `my_crate::my_loader::MyLoader`.
fn id(&self) -> &str;
/// Try loading the texture from the given uri.
///
/// Implementations should call `ctx.request_repaint` to wake up the ui
@ -303,6 +431,12 @@ pub trait TextureLoader {
/// so that it may be fully reloaded.
fn forget(&self, uri: &str);
/// Forget all URIs ever given to this loader.
///
/// If the loader caches any URIs, the entire cache should be cleared,
/// so that all of them may be fully reloaded.
fn forget_all(&self);
/// 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) {
@ -319,25 +453,23 @@ pub(crate) struct DefaultBytesLoader {
}
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()));
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 }),
Some(bytes) => Ok(BytesPoll::Ready {
size: None,
bytes,
mime: None,
}),
None => Err(LoadError::NotSupported),
}
}
@ -346,6 +478,10 @@ impl BytesLoader for DefaultBytesLoader {
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()
}
@ -357,6 +493,10 @@ struct DefaultTextureLoader {
}
impl TextureLoader for DefaultTextureLoader {
fn id(&self) -> &str {
generate_loader_id!(DefaultTextureLoader)
}
fn load(
&self,
ctx: &Context,
@ -385,6 +525,10 @@ impl TextureLoader for DefaultTextureLoader {
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 {
@ -396,22 +540,26 @@ impl TextureLoader for DefaultTextureLoader {
}
}
type BytesLoaderImpl = Arc<dyn BytesLoader + Send + Sync + 'static>;
type ImageLoaderImpl = Arc<dyn ImageLoader + Send + Sync + 'static>;
type TextureLoaderImpl = Arc<dyn TextureLoader + Send + Sync + 'static>;
#[derive(Clone)]
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>>,
pub bytes: Mutex<Vec<BytesLoaderImpl>>,
pub image: Mutex<Vec<ImageLoaderImpl>>,
pub texture: Mutex<Vec<TextureLoaderImpl>>,
}
impl Default for Loaders {
fn default() -> Self {
let include = Arc::new(DefaultBytesLoader::default());
Self {
bytes: vec![include.clone()],
image: Vec::new(),
bytes: Mutex::new(vec![include.clone()]),
image: Mutex::new(Vec::new()),
// By default we only include `DefaultTextureLoader`.
texture: vec![Arc::new(DefaultTextureLoader::default())],
texture: Mutex::new(vec![Arc::new(DefaultTextureLoader::default())]),
include,
}
}

View File

@ -111,7 +111,7 @@ pub fn menu_button<R>(
/// Returns `None` if the menu is not open.
pub fn menu_image_button<R>(
ui: &mut Ui,
image_button: ImageButton,
image_button: ImageButton<'_>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
stationary_menu_image_impl(ui, image_button, Box::new(add_contents))
@ -201,7 +201,7 @@ fn stationary_menu_impl<'c, R>(
/// Responds to primary clicks.
fn stationary_menu_image_impl<'c, R>(
ui: &mut Ui,
image_button: ImageButton,
image_button: ImageButton<'_>,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<Option<R>> {
let bar_id = ui.id();

View File

@ -5,6 +5,7 @@ use std::sync::Arc;
use epaint::mutex::RwLock;
use crate::load::SizedTexture;
use crate::{
containers::*, ecolor::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer,
util::IdTypeMap, widgets::*, *,
@ -1558,39 +1559,6 @@ impl Ui {
response
}
/// Show an image here with the given size.
///
/// In order to display an image you must first acquire a [`TextureHandle`].
/// This is best done with [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html) or [`Context::load_texture`].
///
/// ```
/// struct MyImage {
/// texture: Option<egui::TextureHandle>,
/// }
///
/// impl MyImage {
/// fn ui(&mut self, ui: &mut egui::Ui) {
/// let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
/// // Load the texture only once.
/// ui.ctx().load_texture(
/// "my-image",
/// egui::ColorImage::example(),
/// Default::default()
/// )
/// });
///
/// // Show the image:
/// ui.image(texture, texture.size_vec2());
/// }
/// }
/// ```
///
/// See also [`crate::Image`] and [`crate::ImageButton`].
#[inline]
pub fn image(&mut self, texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Response {
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!
@ -1600,14 +1568,61 @@ impl Ui {
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.image2("file://ferris.svg");
/// ui.image("https://picsum.photos/480");
/// ui.image("file://assets/ferris.png");
/// ui.image(egui::include_image!("../assets/ferris.png"));
/// ui.add(
/// egui::Image::new(egui::include_image!("../assets/ferris.png"))
/// .rounding(egui::Rounding::same(6.0))
/// );
/// # });
/// ```
///
/// See also [`crate::Image2`] and [`crate::ImageSource`].
/// Note: Prefer `include_image` as a source if you're loading an image
/// from a file with a statically known path, unless you really want to
/// load it at runtime instead!
///
/// See also [`crate::Image`], [`crate::ImageSource`] and [`Self::raw_image`].
#[inline]
pub fn image2<'a>(&mut self, source: impl Into<ImageSource<'a>>) -> Response {
Image2::new(source.into()).ui(self)
pub fn image<'a>(&mut self, source: impl Into<ImageSource<'a>>) -> Response {
Image::new(source.into()).ui(self)
}
/// Show an image created from a sized texture.
///
/// You may use this method over [`Ui::image`] if you already have a [`TextureHandle`]
/// or a [`SizedTexture`].
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// struct MyImage {
/// texture: Option<egui::TextureHandle>,
/// }
///
/// impl MyImage {
/// fn ui(&mut self, ui: &mut egui::Ui) {
/// let texture = self
/// .texture
/// .get_or_insert_with(|| {
/// // Load the texture only once.
/// ui.ctx().load_texture(
/// "my-image",
/// egui::ColorImage::example(),
/// Default::default()
/// )
/// });
///
/// // Show the image:
/// ui.raw_image((texture.id(), texture.size_vec2()));
/// }
/// }
/// # });
/// ```
///
/// See also [`crate::RawImage`].
#[inline]
pub fn raw_image(&mut self, texture: impl Into<SizedTexture>) -> Response {
RawImage::new(texture).ui(self)
}
}
@ -2206,15 +2221,9 @@ impl Ui {
/// If called from within a menu this will instead create a button for a sub-menu.
///
/// ```ignore
/// use egui_extras;
/// let img = egui::include_image!("../assets/ferris.png");
///
/// let img = egui_extras::RetainedImage::from_svg_bytes_with_size(
/// "rss",
/// include_bytes!("rss.svg"),
/// egui_extras::image::FitTo::Size(24, 24),
/// );
///
/// ui.menu_image_button(img.texture_id(ctx), img.size_vec2(), |ui| {
/// ui.menu_image_button(img, |ui| {
/// ui.menu_button("My sub-menu", |ui| {
/// if ui.button("Close the menu").clicked() {
/// ui.close_menu();
@ -2225,16 +2234,15 @@ impl Ui {
///
/// See also: [`Self::close_menu`] and [`Response::context_menu`].
#[inline]
pub fn menu_image_button<R>(
pub fn menu_image_button<'a, R>(
&mut self,
texture_id: TextureId,
image_size: impl Into<Vec2>,
image_source: impl Into<ImageSource<'a>>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
if let Some(menu_state) = self.menu_state.clone() {
menu::submenu_button(self, menu_state, String::new(), add_contents)
} else {
menu::menu_image_button(self, ImageButton::new(texture_id, image_size), add_contents)
menu::menu_image_button(self, ImageButton::new(image_source), add_contents)
}
}
}

View File

@ -1,3 +1,5 @@
use crate::load::SizedTexture;
use crate::load::TexturePoll;
use crate::*;
/// Clickable button with text.
@ -32,7 +34,7 @@ pub struct Button {
frame: Option<bool>,
min_size: Vec2,
rounding: Option<Rounding>,
image: Option<widgets::Image>,
image: Option<widgets::RawImage>,
}
impl Button {
@ -60,7 +62,10 @@ impl Button {
text: impl Into<WidgetText>,
) -> Self {
Self {
image: Some(widgets::Image::new(texture_id, image_size)),
image: Some(widgets::RawImage::new(SizedTexture {
id: texture_id,
size: image_size.into(),
})),
..Self::new(text)
}
}
@ -161,7 +166,7 @@ impl Widget for Button {
}
let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x;
if let Some(image) = image {
if let Some(image) = &image {
text_wrap_width -= image.size().x + ui.spacing().icon_spacing;
}
if !shortcut_text.is_empty() {
@ -173,7 +178,7 @@ impl Widget for Button {
.then(|| shortcut_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button));
let mut desired_size = text.size();
if let Some(image) = image {
if let Some(image) = &image {
desired_size.x += image.size().x + ui.spacing().icon_spacing;
desired_size.y = desired_size.y.max(image.size().y);
}
@ -201,7 +206,7 @@ impl Widget for Button {
.rect(rect.expand(visuals.expansion), rounding, fill, stroke);
}
let text_pos = if let Some(image) = image {
let text_pos = if let Some(image) = &image {
let icon_spacing = ui.spacing().icon_spacing;
pos2(
rect.min.x + button_padding.x + image.size().x + icon_spacing,
@ -226,7 +231,7 @@ impl Widget for Button {
);
}
if let Some(image) = image {
if let Some(image) = &image {
let image_rect = Rect::from_min_size(
pos2(
rect.min.x + button_padding.x,
@ -468,17 +473,17 @@ impl Widget for RadioButton {
/// A clickable image within a frame.
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
#[derive(Clone, Debug)]
pub struct ImageButton {
image: widgets::Image,
pub struct ImageButton<'a> {
image: Image<'a>,
sense: Sense,
frame: bool,
selected: bool,
}
impl ImageButton {
pub fn new(texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Self {
impl<'a> ImageButton<'a> {
pub fn new(source: impl Into<ImageSource<'a>>) -> Self {
Self {
image: widgets::Image::new(texture_id, size),
image: Image::new(source.into()),
sense: Sense::click(),
frame: true,
selected: false,
@ -515,29 +520,21 @@ impl ImageButton {
self.sense = sense;
self
}
}
impl Widget for ImageButton {
fn ui(self, ui: &mut Ui) -> Response {
let Self {
image,
sense,
frame,
selected,
} = self;
let padding = if frame {
fn show(&self, ui: &mut Ui, texture: &SizedTexture) -> Response {
let padding = if self.frame {
// so we can see that it is a button:
Vec2::splat(ui.spacing().button_padding.x)
} else {
Vec2::ZERO
};
let padded_size = image.size() + 2.0 * padding;
let (rect, response) = ui.allocate_exact_size(padded_size, sense);
let padded_size = texture.size + 2.0 * padding;
let (rect, response) = ui.allocate_exact_size(padded_size, self.sense);
response.widget_info(|| WidgetInfo::new(WidgetType::ImageButton));
if ui.is_rect_visible(rect) {
let (expansion, rounding, fill, stroke) = if selected {
let (expansion, rounding, fill, stroke) = if self.selected {
let selection = ui.visuals().selection;
(
Vec2::ZERO,
@ -545,7 +542,7 @@ impl Widget for ImageButton {
selection.bg_fill,
selection.stroke,
)
} else if frame {
} else if self.frame {
let visuals = ui.style().interact(&response);
let expansion = Vec2::splat(visuals.expansion);
(
@ -558,17 +555,19 @@ impl Widget for ImageButton {
Default::default()
};
let image = image.rounding(rounding); // apply rounding to the image
// Draw frame background (for transparent images):
ui.painter()
.rect_filled(rect.expand2(expansion), rounding, fill);
let image_rect = ui
.layout()
.align_size_within_rect(image.size(), rect.shrink2(padding));
.align_size_within_rect(texture.size, rect.shrink2(padding));
// let image_rect = image_rect.expand2(expansion); // can make it blurry, so let's not
image.paint_at(ui, image_rect);
let image_options = ImageOptions {
rounding,
..Default::default()
}; // apply rounding to the image
crate::widgets::image::paint_image_at(ui, image_rect, &image_options, texture);
// Draw frame outline:
ui.painter()
@ -578,3 +577,17 @@ impl Widget for ImageButton {
response
}
}
impl<'a> Widget for ImageButton<'a> {
fn ui(self, ui: &mut Ui) -> Response {
match self.image.load(ui) {
Ok(TexturePoll::Ready { texture }) => self.show(ui, &texture),
Ok(TexturePoll::Pending { .. }) => ui
.spinner()
.on_hover_text(format!("Loading {:?}", self.image.uri())),
Err(err) => ui
.colored_label(ui.visuals().error_fg_color, "")
.on_hover_text(err.to_string()),
}
}
}

View File

@ -1,90 +1,176 @@
use std::sync::Arc;
use std::borrow::Cow;
use crate::load::Bytes;
use crate::{load::SizeHint, load::TexturePoll, *};
use crate::load::TextureLoadResult;
use crate::{
load::{Bytes, SizeHint, SizedTexture, TexturePoll},
*,
};
use emath::Rot2;
use epaint::{util::FloatOrd, RectShape};
/// An widget to show an image of a given size.
/// A widget which displays an image.
///
/// In order to display an image you must first acquire a [`TextureHandle`].
/// This is best done with [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html) or [`Context::load_texture`].
/// The task of actually loading the image is deferred to when the `Image` is added to the [`Ui`],
/// and how it is loaded depends on the provided [`ImageSource`]:
///
/// ```
/// struct MyImage {
/// texture: Option<egui::TextureHandle>,
/// }
/// - [`ImageSource::Uri`] will load the image using the [asynchronous loading process][`load`].
/// - [`ImageSource::Bytes`] will also load the image using the [asynchronous loading process][`load`], but with lower latency.
/// - [`ImageSource::Texture`] will use the provided texture.
///
/// impl MyImage {
/// fn ui(&mut self, ui: &mut egui::Ui) {
/// let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
/// // Load the texture only once.
/// ui.ctx().load_texture(
/// "my-image",
/// egui::ColorImage::example(),
/// Default::default()
/// )
/// });
/// To use a texture you already have with a simpler API, consider using [`RawImage`].
///
/// // Show the image:
/// ui.add(egui::Image::new(texture, texture.size_vec2()));
///
/// // Shorter version:
/// ui.image(texture, texture.size_vec2());
/// }
/// }
/// ```
///
/// Se also [`crate::Ui::image`] and [`crate::ImageButton`].
/// See [`load`] for more information.
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
#[derive(Clone, Copy, Debug)]
pub struct Image {
texture_id: TextureId,
uv: Rect,
size: Vec2,
bg_fill: Color32,
tint: Color32,
#[derive(Debug, Clone)]
pub struct Image<'a> {
source: ImageSource<'a>,
texture_options: TextureOptions,
image_options: ImageOptions,
sense: Sense,
rotation: Option<(Rot2, Vec2)>,
rounding: Rounding,
size: ImageSize,
}
impl Image {
pub fn new(texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Self {
impl<'a> Image<'a> {
/// Load the image from some source.
pub fn new(source: ImageSource<'a>) -> Self {
Self {
texture_id: texture_id.into(),
uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
size: size.into(),
bg_fill: Default::default(),
tint: Color32::WHITE,
source,
texture_options: Default::default(),
image_options: Default::default(),
sense: Sense::hover(),
rotation: None,
rounding: Rounding::ZERO,
size: Default::default(),
}
}
/// Load the image from a URI.
///
/// See [`ImageSource::Uri`].
pub fn from_uri(uri: impl Into<Cow<'a, str>>) -> Self {
Self::new(ImageSource::Uri(uri.into()))
}
/// Load the image from an existing texture.
///
/// See [`ImageSource::Texture`].
pub fn from_texture(texture: SizedTexture) -> Self {
Self::new(ImageSource::Texture(texture))
}
/// Load the image from some raw bytes.
///
/// See [`ImageSource::Bytes`].
pub fn from_bytes(uri: &'static str, bytes: impl Into<Bytes>) -> Self {
Self::new(ImageSource::Bytes(uri, bytes.into()))
}
/// Texture options used when creating the texture.
#[inline]
pub fn texture_options(mut self, texture_options: TextureOptions) -> Self {
self.texture_options = texture_options;
self
}
/// Set the max width of the image.
///
/// No matter what the image is scaled to, it will never exceed this limit.
#[inline]
pub fn max_width(mut self, width: f32) -> Self {
match self.size.max_size.as_mut() {
Some(max_size) => max_size.x = width,
None => self.size.max_size = Some(Vec2::new(width, f32::INFINITY)),
}
self
}
/// Set the max height of the image.
///
/// No matter what the image is scaled to, it will never exceed this limit.
#[inline]
pub fn max_height(mut self, height: f32) -> Self {
match self.size.max_size.as_mut() {
Some(max_size) => max_size.y = height,
None => self.size.max_size = Some(Vec2::new(f32::INFINITY, height)),
}
self
}
/// Set the max size of the image.
///
/// No matter what the image is scaled to, it will never exceed this limit.
#[inline]
pub fn max_size(mut self, size: Option<Vec2>) -> Self {
self.size.max_size = size;
self
}
/// Whether or not the [`ImageFit`] should maintain the image's original aspect ratio.
#[inline]
pub fn maintain_aspect_ratio(mut self, value: bool) -> Self {
self.size.maintain_aspect_ratio = value;
self
}
/// Fit the image to its original size.
///
/// This will cause the image to overflow if it is larger than the available space.
///
/// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit.
#[inline]
pub fn fit_to_original_size(mut self, scale: Option<f32>) -> Self {
self.size.fit = ImageFit::Original(scale);
self
}
/// Fit the image to an exact size.
///
/// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit.
#[inline]
pub fn fit_to_exact_size(mut self, size: Vec2) -> Self {
self.size.fit = ImageFit::Exact(size);
self
}
/// Fit the image to a fraction of the available space.
///
/// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit.
#[inline]
pub fn fit_to_fraction(mut self, fraction: Vec2) -> Self {
self.size.fit = ImageFit::Fraction(fraction);
self
}
/// Fit the image to 100% of its available size, shrinking it if necessary.
///
/// This is a shorthand for [`Image::fit_to_fraction`] with `1.0` for both width and height.
///
/// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit.
#[inline]
pub fn shrink_to_fit(self) -> Self {
self.fit_to_fraction(Vec2::new(1.0, 1.0))
}
/// Make the image respond to clicks and/or drags.
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
/// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
self.uv = uv.into();
self.image_options.uv = uv.into();
self
}
/// A solid color to put behind the image. Useful for transparent images.
pub fn bg_fill(mut self, bg_fill: impl Into<Color32>) -> Self {
self.bg_fill = bg_fill.into();
self.image_options.bg_fill = bg_fill.into();
self
}
/// Multiply image color with this. Default is WHITE (no tint).
pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
self.tint = tint.into();
self
}
/// Make the image respond to clicks and/or drags.
///
/// Consider using [`ImageButton`] instead, for an on-hover effect.
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self.image_options.tint = tint.into();
self
}
@ -98,8 +184,8 @@ impl Image {
/// Due to limitations in the current implementation,
/// this will turn off rounding of the image.
pub fn rotate(mut self, angle: f32, origin: Vec2) -> Self {
self.rotation = Some((Rot2::from_angle(angle), origin));
self.rounding = Rounding::ZERO; // incompatible with rotation
self.image_options.rotation = Some((Rot2::from_angle(angle), origin));
self.image_options.rounding = Rounding::ZERO; // incompatible with rotation
self
}
@ -110,144 +196,265 @@ impl Image {
/// Due to limitations in the current implementation,
/// this will turn off any rotation of the image.
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
self.rounding = rounding.into();
if self.rounding != Rounding::ZERO {
self.rotation = None; // incompatible with rounding
self.image_options.rounding = rounding.into();
if self.image_options.rounding != Rounding::ZERO {
self.image_options.rotation = None; // incompatible with rounding
}
self
}
}
impl Image {
pub fn size(&self) -> Vec2 {
self.size
impl<'a> Image<'a> {
/// Returns the size the image will occupy in the final UI.
pub fn calculate_size(&self, available_size: Vec2, image_size: Vec2) -> Vec2 {
self.size.get(available_size, image_size)
}
pub fn paint_at(&self, ui: &mut Ui, rect: Rect) {
if ui.is_rect_visible(rect) {
use epaint::*;
let Self {
texture_id,
uv,
size,
bg_fill,
tint,
sense: _,
rotation,
rounding,
} = self;
pub fn size(&self) -> Option<Vec2> {
match &self.source {
ImageSource::Texture(texture) => Some(texture.size),
ImageSource::Uri(_) | ImageSource::Bytes(_, _) => None,
}
}
if *bg_fill != Default::default() {
let mut mesh = Mesh::default();
mesh.add_colored_rect(rect, *bg_fill);
ui.painter().add(Shape::mesh(mesh));
}
pub fn source(&self) -> &ImageSource<'a> {
&self.source
}
if let Some((rot, origin)) = rotation {
// TODO(emilk): implement this using `PathShape` (add texture support to it).
// This will also give us anti-aliasing of rotated images.
egui_assert!(
*rounding == Rounding::ZERO,
"Image had both rounding and rotation. Please pick only one"
);
/// Get the `uri` that this image was constructed from.
///
/// This will return `<unknown>` for [`ImageSource::Texture`].
pub fn uri(&self) -> &str {
match &self.source {
ImageSource::Bytes(uri, _) => uri,
ImageSource::Uri(uri) => uri,
// Note: texture source is never in "loading" state
ImageSource::Texture(_) => "<unknown>",
}
}
let mut mesh = Mesh::with_texture(*texture_id);
mesh.add_rect_with_uv(rect, *uv, *tint);
mesh.rotate(*rot, rect.min + *origin * *size);
ui.painter().add(Shape::mesh(mesh));
} else {
ui.painter().add(RectShape {
rect,
rounding: *rounding,
fill: *tint,
stroke: Stroke::NONE,
fill_texture_id: *texture_id,
uv: *uv,
});
/// Load the image from its [`Image::source`], returning the resulting [`SizedTexture`].
///
/// # Errors
///
/// May fail if they underlying [`Context::try_load_texture`] call fails.
pub fn load(&self, ui: &Ui) -> TextureLoadResult {
match self.source.clone() {
ImageSource::Texture(texture) => Ok(TexturePoll::Ready { texture }),
ImageSource::Uri(uri) => ui.ctx().try_load_texture(
uri.as_ref(),
self.texture_options,
self.size.hint(ui.available_size()),
),
ImageSource::Bytes(uri, bytes) => {
ui.ctx().include_bytes(uri.as_ref(), bytes);
ui.ctx().try_load_texture(
uri.as_ref(),
self.texture_options,
self.size.hint(ui.available_size()),
)
}
}
}
pub fn paint_at(&self, ui: &mut Ui, rect: Rect, texture: &SizedTexture) {
paint_image_at(ui, rect, &self.image_options, texture);
}
}
impl Widget for Image {
impl<'a> Widget for Image<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let (rect, response) = ui.allocate_exact_size(self.size, self.sense);
self.paint_at(ui, rect);
response
match self.load(ui) {
Ok(TexturePoll::Ready { texture }) => {
let size = self.calculate_size(ui.available_size(), texture.size);
let (rect, response) = ui.allocate_exact_size(size, self.sense);
self.paint_at(ui, rect, &texture);
response
}
Ok(TexturePoll::Pending { size }) => match size {
Some(size) => {
let size = self.calculate_size(ui.available_size(), size);
ui.allocate_ui(size, |ui| {
ui.with_layout(Layout::centered_and_justified(Direction::TopDown), |ui| {
ui.spinner()
.on_hover_text(format!("Loading {:?}", self.uri()))
})
})
.response
}
None => ui
.spinner()
.on_hover_text(format!("Loading {:?}", self.uri())),
},
Err(err) => ui
.colored_label(ui.visuals().error_fg_color, "")
.on_hover_text(err.to_string()),
}
}
}
/// 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,
/// This type determines the constraints on how
/// the size of an image should be calculated.
#[derive(Debug, Clone, Copy)]
pub struct ImageSize {
/// Whether or not the final size should maintain the original aspect ratio.
///
/// This setting is applied last.
///
/// This defaults to `true`.
pub maintain_aspect_ratio: bool,
/// Determines the maximum size of the image.
///
/// Defaults to `None`
pub max_size: Option<Vec2>,
/// Determines how the image should shrink/expand/stretch/etc. to fit within its allocated space.
///
/// This setting is applied first.
///
/// Defaults to `ImageFit::Fraction([1, 1])`
pub fit: ImageFit,
}
#[derive(Default, Clone, Copy)]
enum ImageFit {
// TODO: options for aspect ratio
// TODO: other fit strategies
// FitToWidth,
// FitToHeight,
// FitToWidthExact(f32),
// FitToHeightExact(f32),
#[default]
ShrinkToFit,
/// This type determines how the image should try to fit within the UI.
///
/// The final fit will be clamped to [`ImageSize::max_size`].
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ImageFit {
/// Fit the image to its original size, optionally scaling it by some factor.
Original(Option<f32>),
/// Fit the image to a fraction of the available size.
Fraction(Vec2),
/// Fit the image to an exact size.
Exact(Vec2),
}
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)
impl ImageSize {
fn hint(&self, available_size: Vec2) -> SizeHint {
if self.maintain_aspect_ratio {
return SizeHint::Scale(1.0.ord());
};
let fit = match self.fit {
ImageFit::Original(scale) => return SizeHint::Scale(scale.unwrap_or(1.0).ord()),
ImageFit::Fraction(fract) => available_size * fract,
ImageFit::Exact(size) => size,
};
let fit = match self.max_size {
Some(extent) => fit.min(extent),
None => fit,
};
// `inf` on an axis means "any value"
match (fit.x.is_finite(), fit.y.is_finite()) {
(true, true) => SizeHint::Size(fit.x.round() as u32, fit.y.round() as u32),
(true, false) => SizeHint::Width(fit.x.round() as u32),
(false, true) => SizeHint::Height(fit.y.round() as u32),
(false, false) => SizeHint::Scale(1.0.ord()),
}
}
fn get(&self, available_size: Vec2, image_size: Vec2) -> Vec2 {
match self.fit {
ImageFit::Original(scale) => {
let image_size = image_size * scale.unwrap_or(1.0);
if let Some(available_size) = self.max_size {
if image_size.x < available_size.x && image_size.y < available_size.y {
return image_size;
}
if self.maintain_aspect_ratio {
let ratio_x = available_size.x / image_size.x;
let ratio_y = available_size.y / image_size.y;
let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y };
let ratio = if ratio.is_infinite() { 1.0 } else { ratio };
return Vec2::new(image_size.x * ratio, image_size.y * ratio);
} else {
return image_size.min(available_size);
}
}
image_size
}
ImageFit::Fraction(fract) => {
let available_size = available_size * fract;
let available_size = match self.max_size {
Some(max_size) => available_size.min(max_size),
None => available_size,
};
if self.maintain_aspect_ratio {
let ratio_x = available_size.x / image_size.x;
let ratio_y = available_size.y / image_size.y;
let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y };
let ratio = if ratio.is_infinite() { 1.0 } else { ratio };
return Vec2::new(image_size.x * ratio, image_size.y * ratio);
}
available_size
}
ImageFit::Exact(size) => {
let available_size = size;
let available_size = match self.max_size {
Some(max_size) => available_size.min(max_size),
None => available_size,
};
if self.maintain_aspect_ratio {
let ratio_x = available_size.x / image_size.x;
let ratio_y = available_size.y / image_size.y;
let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y };
let ratio = if ratio.is_infinite() { 1.0 } else { ratio };
return Vec2::new(image_size.x * ratio, image_size.y * ratio);
}
available_size
}
}
}
}
/// This type tells the [`Ui`] how to load the image.
impl Default for ImageSize {
#[inline]
fn default() -> Self {
Self {
max_size: None,
fit: ImageFit::Fraction(Vec2::new(1.0, 1.0)),
maintain_aspect_ratio: true,
}
}
}
/// This type tells the [`Ui`] how to load an image.
///
/// This is used by [`Image::new`] and [`Ui::image`].
#[derive(Debug, Clone)]
pub enum ImageSource<'a> {
/// Load the image from a URI.
///
/// This could be a `file://` url, `http://` url, or a `bare` identifier.
/// This could be a `file://` url, `http(s)?://` 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),
Uri(Cow<'a, str>),
/// Load the image from an existing texture.
///
/// The user is responsible for loading the texture, determining its size,
/// and allocating a [`TextureId`] for it.
///
/// Note that a simpler API for this exists in [`RawImage`].
Texture(SizedTexture),
/// Load the image from some raw bytes.
///
@ -257,6 +464,8 @@ pub enum ImageSource<'a> {
///
/// This instructs the [`Ui`] to cache the raw bytes, which are then further processed by any registered loaders.
///
/// See also [`include_image`] for an easy way to load and display static images.
///
/// See [`crate::load`] for more information.
Bytes(&'static str, Bytes),
}
@ -264,6 +473,33 @@ pub enum ImageSource<'a> {
impl<'a> From<&'a str> for ImageSource<'a> {
#[inline]
fn from(value: &'a str) -> Self {
Self::Uri(value.into())
}
}
impl<'a> From<&'a String> for ImageSource<'a> {
#[inline]
fn from(value: &'a String) -> Self {
Self::Uri(value.as_str().into())
}
}
impl From<String> for ImageSource<'static> {
fn from(value: String) -> Self {
Self::Uri(value.into())
}
}
impl<'a> From<&'a Cow<'a, str>> for ImageSource<'a> {
#[inline]
fn from(value: &'a Cow<'a, str>) -> Self {
Self::Uri(value.clone())
}
}
impl<'a> From<Cow<'a, str>> for ImageSource<'a> {
#[inline]
fn from(value: Cow<'a, str>) -> Self {
Self::Uri(value)
}
}
@ -275,55 +511,29 @@ impl<T: Into<Bytes>> From<(&'static str, T)> for ImageSource<'static> {
}
}
impl<'a> Image2<'a> {
impl<T: Into<SizedTexture>> From<T> for ImageSource<'static> {
fn from(value: T) -> Self {
Self::Texture(value.into())
}
}
/// A widget which displays a sized texture.
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
#[derive(Debug, Clone)]
pub struct RawImage {
texture: SizedTexture,
texture_options: TextureOptions,
image_options: ImageOptions,
sense: Sense,
}
impl RawImage {
/// Load the image from some source.
pub fn new(source: ImageSource<'a>) -> Self {
pub fn new(texture: impl Into<SizedTexture>) -> Self {
Self {
source,
texture: texture.into(),
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(),
image_options: Default::default(),
sense: Sense::hover(),
}
}
@ -335,60 +545,164 @@ impl<'a> Image2<'a> {
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
}
/// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
self.image_options.uv = uv.into();
self
}
/// A solid color to put behind the image. Useful for transparent images.
pub fn bg_fill(mut self, bg_fill: impl Into<Color32>) -> Self {
self.image_options.bg_fill = bg_fill.into();
self
}
/// Multiply image color with this. Default is WHITE (no tint).
pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
self.image_options.tint = tint.into();
self
}
/// Rotate the image about an origin by some angle
///
/// Positive angle is clockwise.
/// Origin is a vector in normalized UV space ((0,0) in top-left, (1,1) bottom right).
///
/// To rotate about the center you can pass `Vec2::splat(0.5)` as the origin.
///
/// Due to limitations in the current implementation,
/// this will turn off rounding of the image.
pub fn rotate(mut self, angle: f32, origin: Vec2) -> Self {
self.image_options.rotation = Some((Rot2::from_angle(angle), origin));
self.image_options.rounding = Rounding::ZERO; // incompatible with rotation
self
}
/// Round the corners of the image.
///
/// The default is no rounding ([`Rounding::ZERO`]).
///
/// Due to limitations in the current implementation,
/// this will turn off any rotation of the image.
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
self.image_options.rounding = rounding.into();
if self.image_options.rounding != Rounding::ZERO {
self.image_options.rotation = None; // incompatible with rounding
}
self
}
}
impl<'a> Widget for Image2<'a> {
impl RawImage {
/// Returns the [`TextureId`] of the texture from which this image was created.
pub fn texture_id(&self) -> TextureId {
self.texture.id
}
/// Returns the size of the texture from which this image was created.
pub fn size(&self) -> Vec2 {
self.texture.size
}
pub fn paint_at(&self, ui: &mut Ui, rect: Rect) {
paint_image_at(ui, rect, &self.image_options, &self.texture);
}
}
impl Widget for RawImage {
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
}
};
let (rect, response) = ui.allocate_exact_size(self.size(), self.sense);
self.paint_at(ui, rect);
response
}
}
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),
);
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ImageOptions {
/// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
pub uv: Rect,
let (rect, response) = ui.allocate_exact_size(final_size, self.sense);
/// A solid color to put behind the image. Useful for transparent images.
pub bg_fill: Color32,
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));
/// Multiply image color with this. Default is WHITE (no tint).
pub tint: Color32,
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()),
/// Rotate the image about an origin by some angle
///
/// Positive angle is clockwise.
/// Origin is a vector in normalized UV space ((0,0) in top-left, (1,1) bottom right).
///
/// To rotate about the center you can pass `Vec2::splat(0.5)` as the origin.
///
/// Due to limitations in the current implementation,
/// this will turn off rounding of the image.
pub rotation: Option<(Rot2, Vec2)>,
/// Round the corners of the image.
///
/// The default is no rounding ([`Rounding::ZERO`]).
///
/// Due to limitations in the current implementation,
/// this will turn off any rotation of the image.
pub rounding: Rounding,
}
impl Default for ImageOptions {
fn default() -> Self {
Self {
uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
bg_fill: Default::default(),
tint: Color32::WHITE,
rotation: None,
rounding: Rounding::ZERO,
}
}
}
/// Paint a `SizedTexture` as an image according to some `ImageOptions` at a given `rect`.
pub fn paint_image_at(ui: &mut Ui, rect: Rect, options: &ImageOptions, texture: &SizedTexture) {
if !ui.is_rect_visible(rect) {
return;
}
if options.bg_fill != Default::default() {
let mut mesh = Mesh::default();
mesh.add_colored_rect(rect, options.bg_fill);
ui.painter().add(Shape::mesh(mesh));
}
match options.rotation {
Some((rot, origin)) => {
// TODO(emilk): implement this using `PathShape` (add texture support to it).
// This will also give us anti-aliasing of rotated images.
egui_assert!(
options.rounding == Rounding::ZERO,
"Image had both rounding and rotation. Please pick only one"
);
let mut mesh = Mesh::with_texture(texture.id);
mesh.add_rect_with_uv(rect, options.uv, options.tint);
mesh.rotate(rot, rect.min + origin * rect.size());
ui.painter().add(Shape::mesh(mesh));
}
None => {
ui.painter().add(RectShape {
rect,
rounding: options.rounding,
fill: options.tint,
stroke: Stroke::NONE,
fill_texture_id: texture.id,
uv: options.uv,
});
}
}
}

View File

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

View File

@ -19,6 +19,7 @@ crate-type = ["cdylib", "rlib"]
default = ["glow", "persistence"]
http = ["ehttp", "image", "poll-promise", "egui_extras/image"]
image_viewer = ["image", "egui_extras/all-loaders", "rfd"]
persistence = ["eframe/persistence", "egui/persistence", "serde"]
web_screen_reader = ["eframe/web_screen_reader"] # experimental
serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]
@ -27,7 +28,6 @@ syntax_highlighting = ["egui_demo_lib/syntax_highlighting"]
glow = ["eframe/glow"]
wgpu = ["eframe/wgpu", "bytemuck"]
[dependencies]
chrono = { version = "0.4", default-features = false, features = [
"js-sys",
@ -48,6 +48,7 @@ bytemuck = { version = "1.7.1", optional = true }
egui_extras = { version = "0.22.0", optional = true, path = "../egui_extras", features = [
"log",
] }
rfd = { version = "0.11", optional = true }
# feature "http":
ehttp = { version = "0.3.0", optional = true }

View File

@ -1,3 +1,5 @@
#![allow(deprecated)]
use egui_extras::RetainedImage;
use poll_promise::Promise;

View File

@ -0,0 +1,217 @@
use egui::emath::Rot2;
use egui::panel::Side;
use egui::panel::TopBottomSide;
use egui::ImageFit;
use egui::Slider;
use egui::Vec2;
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ImageViewer {
current_uri: String,
uri_edit_text: String,
image_options: egui::ImageOptions,
chosen_fit: ChosenFit,
fit: ImageFit,
maintain_aspect_ratio: bool,
max_size: Option<Vec2>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
enum ChosenFit {
ExactSize,
Fraction,
OriginalSize,
}
impl ChosenFit {
fn as_str(&self) -> &'static str {
match self {
ChosenFit::ExactSize => "exact size",
ChosenFit::Fraction => "fraction",
ChosenFit::OriginalSize => "original size",
}
}
}
impl Default for ImageViewer {
fn default() -> Self {
Self {
current_uri: "https://picsum.photos/seed/1.759706314/1024".to_owned(),
uri_edit_text: "https://picsum.photos/seed/1.759706314/1024".to_owned(),
image_options: egui::ImageOptions::default(),
chosen_fit: ChosenFit::Fraction,
fit: ImageFit::Fraction(Vec2::splat(1.0)),
maintain_aspect_ratio: true,
max_size: None,
}
}
}
impl eframe::App for ImageViewer {
fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
egui::TopBottomPanel::new(TopBottomSide::Top, "url bar").show(ctx, |ui| {
ui.horizontal_centered(|ui| {
ui.label("URI:");
ui.text_edit_singleline(&mut self.uri_edit_text);
if ui.small_button("").clicked() {
ctx.forget_image(&self.current_uri);
self.uri_edit_text = self.uri_edit_text.trim().to_owned();
self.current_uri = self.uri_edit_text.clone();
};
#[cfg(not(target_arch = "wasm32"))]
if ui.button("file…").clicked() {
if let Some(path) = rfd::FileDialog::new().pick_file() {
self.uri_edit_text = format!("file://{}", path.display());
self.current_uri = self.uri_edit_text.clone();
}
}
});
});
egui::SidePanel::new(Side::Left, "controls").show(ctx, |ui| {
// uv
ui.label("UV");
ui.add(Slider::new(&mut self.image_options.uv.min.x, 0.0..=1.0).text("min x"));
ui.add(Slider::new(&mut self.image_options.uv.min.y, 0.0..=1.0).text("min y"));
ui.add(Slider::new(&mut self.image_options.uv.max.x, 0.0..=1.0).text("max x"));
ui.add(Slider::new(&mut self.image_options.uv.max.y, 0.0..=1.0).text("max y"));
// rotation
ui.add_space(2.0);
let had_rotation = self.image_options.rotation.is_some();
let mut has_rotation = had_rotation;
ui.checkbox(&mut has_rotation, "Rotation");
match (had_rotation, has_rotation) {
(true, false) => self.image_options.rotation = None,
(false, true) => {
self.image_options.rotation =
Some((Rot2::from_angle(0.0), Vec2::new(0.5, 0.5)));
}
(true, true) | (false, false) => {}
}
if let Some((rot, origin)) = self.image_options.rotation.as_mut() {
let mut angle = rot.angle();
ui.label("angle");
ui.drag_angle(&mut angle);
*rot = Rot2::from_angle(angle);
ui.add(Slider::new(&mut origin.x, 0.0..=1.0).text("origin x"));
ui.add(Slider::new(&mut origin.y, 0.0..=1.0).text("origin y"));
}
// bg_fill
ui.add_space(2.0);
ui.label("Background color");
ui.color_edit_button_srgba(&mut self.image_options.bg_fill);
// tint
ui.add_space(2.0);
ui.label("Tint");
ui.color_edit_button_srgba(&mut self.image_options.tint);
// fit
ui.add_space(10.0);
ui.label(
"The chosen fit will determine how the image tries to fill the available space",
);
egui::ComboBox::from_label("Fit")
.selected_text(self.chosen_fit.as_str())
.show_ui(ui, |ui| {
ui.selectable_value(
&mut self.chosen_fit,
ChosenFit::ExactSize,
ChosenFit::ExactSize.as_str(),
);
ui.selectable_value(
&mut self.chosen_fit,
ChosenFit::Fraction,
ChosenFit::Fraction.as_str(),
);
ui.selectable_value(
&mut self.chosen_fit,
ChosenFit::OriginalSize,
ChosenFit::OriginalSize.as_str(),
);
});
match self.chosen_fit {
ChosenFit::ExactSize => {
if !matches!(self.fit, ImageFit::Exact(_)) {
self.fit = ImageFit::Exact(Vec2::splat(128.0));
}
let ImageFit::Exact(size) = &mut self.fit else { unreachable!() };
ui.add(Slider::new(&mut size.x, 0.0..=2048.0).text("width"));
ui.add(Slider::new(&mut size.y, 0.0..=2048.0).text("height"));
}
ChosenFit::Fraction => {
if !matches!(self.fit, ImageFit::Fraction(_)) {
self.fit = ImageFit::Fraction(Vec2::splat(1.0));
}
let ImageFit::Fraction(fract) = &mut self.fit else { unreachable!() };
ui.add(Slider::new(&mut fract.x, 0.0..=1.0).text("width"));
ui.add(Slider::new(&mut fract.y, 0.0..=1.0).text("height"));
}
ChosenFit::OriginalSize => {
if !matches!(self.fit, ImageFit::Original(_)) {
self.fit = ImageFit::Original(Some(1.0));
}
let ImageFit::Original(Some(scale)) = &mut self.fit else { unreachable!() };
ui.add(Slider::new(scale, 0.1..=4.0).text("scale"));
}
}
// max size
ui.add_space(5.0);
ui.label("The calculated size will not exceed the maximum size");
let had_max_size = self.max_size.is_some();
let mut has_max_size = had_max_size;
ui.checkbox(&mut has_max_size, "Max size");
match (had_max_size, has_max_size) {
(true, false) => self.max_size = None,
(false, true) => {
self.max_size = Some(ui.available_size());
}
(true, true) | (false, false) => {}
}
if let Some(max_size) = self.max_size.as_mut() {
ui.add(Slider::new(&mut max_size.x, 0.0..=2048.0).text("width"));
ui.add(Slider::new(&mut max_size.y, 0.0..=2048.0).text("height"));
}
// aspect ratio
ui.add_space(5.0);
ui.label("Aspect ratio is maintained by scaling both sides as necessary");
ui.checkbox(&mut self.maintain_aspect_ratio, "Maintain aspect ratio");
});
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::new([true, true]).show(ui, |ui| {
let mut image = egui::Image::from_uri(&self.current_uri);
image = image.uv(self.image_options.uv);
image = image.bg_fill(self.image_options.bg_fill);
image = image.tint(self.image_options.tint);
let (angle, origin) = self
.image_options
.rotation
.map_or((0.0, Vec2::splat(0.5)), |(rot, origin)| {
(rot.angle(), origin)
});
image = image.rotate(angle, origin);
match self.fit {
ImageFit::Original(scale) => image = image.fit_to_original_size(scale),
ImageFit::Fraction(fract) => image = image.fit_to_fraction(fract),
ImageFit::Exact(size) => image = image.fit_to_exact_size(size),
}
image = image.maintain_aspect_ratio(self.maintain_aspect_ratio);
image = image.max_size(self.max_size);
ui.add_sized(ui.available_size(), image);
});
});
}
}

View File

@ -9,6 +9,12 @@ mod fractal_clock;
#[cfg(feature = "http")]
mod http_app;
#[cfg(feature = "image_viewer")]
mod image_viewer;
#[cfg(feature = "image_viewer")]
pub use image_viewer::ImageViewer;
#[cfg(all(feature = "glow", not(feature = "wgpu")))]
pub use custom3d_glow::Custom3d;

View File

@ -82,6 +82,8 @@ enum Anchor {
EasyMarkEditor,
#[cfg(feature = "http")]
Http,
#[cfg(feature = "image_viewer")]
ImageViewer,
Clock,
#[cfg(any(feature = "glow", feature = "wgpu"))]
Custom3d,
@ -142,6 +144,8 @@ pub struct State {
easy_mark_editor: EasyMarkApp,
#[cfg(feature = "http")]
http: crate::apps::HttpApp,
#[cfg(feature = "image_viewer")]
image_viewer: crate::apps::ImageViewer,
clock: FractalClockApp,
color_test: ColorTestApp,
@ -161,6 +165,9 @@ pub struct WrapApp {
impl WrapApp {
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
#[cfg(feature = "image_viewer")]
egui_extras::loaders::install(&_cc.egui_ctx);
#[allow(unused_mut)]
let mut slf = Self {
state: State::default(),
@ -204,6 +211,12 @@ impl WrapApp {
Anchor::Clock,
&mut self.state.clock as &mut dyn eframe::App,
),
#[cfg(feature = "image_viewer")]
(
"🖼️ Image Viewer",
Anchor::ImageViewer,
&mut self.state.image_viewer as &mut dyn eframe::App,
),
];
#[cfg(any(feature = "glow", feature = "wgpu"))]

View File

@ -87,8 +87,12 @@ impl ColorTest {
let tex = self.tex_mngr.get(ui.ctx(), &g);
let texel_offset = 0.5 / (g.0.len() as f32);
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
ui.add(Image::new(tex, GRADIENT_SIZE).tint(vertex_color).uv(uv))
.on_hover_text(format!("A texture that is {} texels wide", g.0.len()));
ui.add(
RawImage::new((tex.id(), GRADIENT_SIZE))
.tint(vertex_color)
.uv(uv),
)
.on_hover_text(format!("A texture that is {} texels wide", g.0.len()));
ui.label("GPU result");
});
});
@ -225,11 +229,15 @@ impl ColorTest {
let tex = self.tex_mngr.get(ui.ctx(), gradient);
let texel_offset = 0.5 / (gradient.0.len() as f32);
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
ui.add(Image::new(tex, GRADIENT_SIZE).bg_fill(bg_fill).uv(uv))
.on_hover_text(format!(
"A texture that is {} texels wide",
gradient.0.len()
));
ui.add(
RawImage::new((tex.id(), GRADIENT_SIZE))
.bg_fill(bg_fill)
.uv(uv),
)
.on_hover_text(format!(
"A texture that is {} texels wide",
gradient.0.len()
));
ui.label(label);
});
}

View File

@ -209,11 +209,14 @@ impl WidgetGallery {
let img_size = 16.0 * texture.size_vec2() / texture.size_vec2().y;
ui.add(doc_link_label("Image", "Image"));
ui.image(texture, img_size);
ui.raw_image((texture.id(), img_size));
ui.end_row();
ui.add(doc_link_label("ImageButton", "ImageButton"));
if ui.add(egui::ImageButton::new(texture, img_size)).clicked() {
if ui
.add(egui::ImageButton::new((texture.id(), img_size)))
.clicked()
{
*boolean = !*boolean;
}
ui.end_row();

View File

@ -24,14 +24,17 @@ all-features = true
[features]
default = []
default = ["dep:mime_guess"]
## Shorthand for enabling `svg`, `image`, and `ehttp`.
all-loaders = ["svg", "image", "http"]
## Shorthand for enabling the different types of image loaders (`file`, `http`, `image`, `svg`).
all-loaders = ["svg", "image", "http", "file"]
## Enable [`DatePickerButton`] widget.
datepicker = ["chrono"]
## Add support for loading images from `file://` URIs.
file = ["dep:mime_guess"]
## Log warnings using [`log`](https://docs.rs/log) crate.
log = ["dep:log", "egui/log"]
@ -75,6 +78,9 @@ image = { version = "0.24", optional = true, default-features = false }
# feature "log"
log = { version = "0.4", optional = true, features = ["std"] }
# file feature
mime_guess = { version = "2.0.4", optional = true, default-features = false }
puffin = { version = "0.16", optional = true }
# svg feature

View File

@ -1,3 +1,5 @@
#![allow(deprecated)]
use egui::{mutex::Mutex, TextureFilter, TextureOptions};
#[cfg(feature = "svg")]
@ -8,6 +10,9 @@ pub use usvg::FitTo;
/// Load once, and save somewhere in your app state.
///
/// Use the `svg` and `image` features to enable more constructors.
///
/// ⚠ This type is deprecated: Consider using [`egui::Image`] instead.
#[deprecated = "consider using `egui::Image` instead"]
pub struct RetainedImage {
debug_name: String,
@ -186,7 +191,7 @@ impl RetainedImage {
// We need to convert the SVG to a texture to display it:
// Future improvement: tell backend to do mip-mapping of the image to
// make it look smoother when downsized.
ui.image(self.texture_id(ui.ctx()), desired_size)
ui.raw_image((self.texture_id(ui.ctx()), desired_size))
}
}

View File

@ -23,6 +23,7 @@ mod table;
#[cfg(feature = "chrono")]
pub use crate::datepicker::DatePickerButton;
#[allow(deprecated)]
pub use crate::image::RetainedImage;
pub(crate) use crate::layout::StripLayout;
pub use crate::sizing::Size;
@ -63,6 +64,7 @@ pub(crate) use profiling_scopes::*;
/// Log an error with either `log` or `eprintln`
macro_rules! log_err {
($fmt: literal) => {$crate::log_err!($fmt,)};
($fmt: literal, $($arg: tt)*) => {{
#[cfg(feature = "log")]
log::error!($fmt, $($arg)*);
@ -77,6 +79,7 @@ pub(crate) use log_err;
/// Panic in debug builds, log otherwise.
macro_rules! log_or_panic {
($fmt: literal) => {$crate::log_or_panic!($fmt,)};
($fmt: literal, $($arg: tt)*) => {{
if cfg!(debug_assertions) {
panic!($fmt, $($arg)*);

View File

@ -1,41 +1,87 @@
// TODO: automatic cache eviction
/// Installs the default set of loaders:
/// Installs the default set of loaders.
///
/// - `file` loader on non-Wasm targets
/// - `http` loader (with the `ehttp` feature)
/// - `http` loader (with the `http` 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.
/// Calling this multiple times on the same [`egui::Context`] is safe.
/// It will never install duplicate loaders.
///
/// ⚠ The supported set of image formats is configured by adding the [`image`](https://crates.io/crates/image)
/// ⚠ 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://` URIs, 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"] }
/// ```
///
/// ⚠ You have to configure both the supported loaders in `egui_extras` _and_ the supported image formats
/// in `image` to get any output!
///
/// ## Loader-specific information
///
/// ⚠ The exact way bytes, images, and textures are loaded is subject to change,
/// but the supported protocols and file extensions are not.
///
/// The `file` loader is a [`BytesLoader`][`egui::load::BytesLoader`].
/// It will attempt to load `file://` URIs, and infer the content type from the extension.
/// The path will be passed to [`std::fs::read`] after trimming the `file://` prefix,
/// and is resolved the same way as with `std::fs::read(path)`:
/// - Relative paths are relative to the current working directory
/// - Absolute paths are left as is.
///
/// The `http` loader is a [`BytesLoader`][`egui::load::BytesLoader`].
/// It will attempt to load `http://` and `https://` URIs, and infer the content type from the `Content-Type` header.
///
/// The `image` loader is an [`ImageLoader`][`egui::load::ImageLoader`].
/// It will attempt to load any URI with any extension other than `svg`. It will also load any URI without an extension.
/// The content type specified by [`BytesPoll::Ready::mime`][`egui::load::BytesPoll::Ready::mime`] always takes precedence.
/// This means that even if the URI has a `png` extension, and the `png` image format is enabled, if the content type is
/// not one of the supported and enabled image formats, the loader will return [`LoadError::NotSupported`][`egui::load::LoadError::NotSupported`],
/// allowing a different loader to attempt to load the image.
///
/// The `svg` loader is an [`ImageLoader`][`egui::load::ImageLoader`].
/// It will attempt to load any URI with an `svg` extension. It will _not_ attempt to load a URI without an extension.
/// The content type specified by [`BytesPoll::Ready::mime`][`egui::load::BytesPoll::Ready::mime`] always takes precedence,
/// and must include `svg` for it to be considered supported. For example, `image/svg+xml` would be loaded by the `svg` loader.
///
/// 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(all(not(target_arch = "wasm32"), feature = "file"))]
if !ctx.is_loader_installed(self::file_loader::FileLoader::ID) {
ctx.add_bytes_loader(std::sync::Arc::new(self::file_loader::FileLoader::default()));
crate::log_trace!("installed FileLoader");
}
#[cfg(feature = "http")]
ctx.add_bytes_loader(std::sync::Arc::new(
self::ehttp_loader::EhttpLoader::default(),
));
if !ctx.is_loader_installed(self::ehttp_loader::EhttpLoader::ID) {
ctx.add_bytes_loader(std::sync::Arc::new(
self::ehttp_loader::EhttpLoader::default(),
));
crate::log_trace!("installed EhttpLoader");
}
#[cfg(feature = "image")]
ctx.add_image_loader(std::sync::Arc::new(
self::image_loader::ImageCrateLoader::default(),
));
if !ctx.is_loader_installed(self::image_loader::ImageCrateLoader::ID) {
ctx.add_image_loader(std::sync::Arc::new(
self::image_loader::ImageCrateLoader::default(),
));
crate::log_trace!("installed ImageCrateLoader");
}
#[cfg(feature = "svg")]
ctx.add_image_loader(std::sync::Arc::new(self::svg_loader::SvgLoader::default()));
if !ctx.is_loader_installed(self::svg_loader::SvgLoader::ID) {
ctx.add_image_loader(std::sync::Arc::new(self::svg_loader::SvgLoader::default()));
crate::log_trace!("installed SvgLoader");
}
#[cfg(all(
target_arch = "wasm32",
any(target_arch = "wasm32", not(feature = "file")),
not(feature = "http"),
not(feature = "image"),
not(feature = "svg")

View File

@ -5,52 +5,60 @@ use egui::{
};
use std::{sync::Arc, task::Poll};
type Entry = Poll<Result<Arc<[u8]>, String>>;
#[derive(Clone)]
struct File {
bytes: Arc<[u8]>,
mime: Option<String>,
}
impl File {
fn from_response(uri: &str, response: ehttp::Response) -> Result<Self, String> {
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 mime = response.content_type().map(|v| v.to_owned());
let bytes = response.bytes.into();
Ok(File { bytes, mime })
}
}
type Entry = Poll<Result<File, String>>;
#[derive(Default)]
pub struct EhttpLoader {
cache: Arc<Mutex<HashMap<String, Entry>>>,
}
impl EhttpLoader {
pub const ID: &str = egui::generate_loader_id!(EhttpLoader);
}
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 id(&self) -> &str {
Self::ID
}
fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult {
if !starts_with_one_of(uri, PROTOCOLS) {
return Err(LoadError::NotSupported);
@ -59,9 +67,10 @@ impl BytesLoader for EhttpLoader {
let mut cache = self.cache.lock();
if let Some(entry) = cache.get(uri).cloned() {
match entry {
Poll::Ready(Ok(bytes)) => Ok(BytesPoll::Ready {
Poll::Ready(Ok(file)) => Ok(BytesPoll::Ready {
size: None,
bytes: Bytes::Shared(bytes),
bytes: Bytes::Shared(file.bytes),
mime: file.mime,
}),
Poll::Ready(Err(err)) => Err(LoadError::Custom(err)),
Poll::Pending => Ok(BytesPoll::Pending { size: None }),
@ -77,7 +86,14 @@ impl BytesLoader for EhttpLoader {
let ctx = ctx.clone();
let cache = self.cache.clone();
move |response| {
let result = get_image_bytes(&uri, response);
let result = match response {
Ok(response) => File::from_response(&uri, response),
Err(err) => {
// Log details; return summary
crate::log_err!("Failed to load {uri:?}: {err}");
Err(format!("Failed to load {uri:?}"))
}
};
crate::log_trace!("finished loading {uri:?}");
let prev = cache.lock().insert(uri, Poll::Ready(result));
assert!(matches!(prev, Some(Poll::Pending)));
@ -93,12 +109,18 @@ impl BytesLoader for EhttpLoader {
let _ = self.cache.lock().remove(uri);
}
fn forget_all(&self) {
self.cache.lock().clear();
}
fn byte_size(&self) -> usize {
self.cache
.lock()
.values()
.map(|entry| match entry {
Poll::Ready(Ok(bytes)) => bytes.len(),
Poll::Ready(Ok(file)) => {
file.bytes.len() + file.mime.as_ref().map_or(0, |m| m.len())
}
Poll::Ready(Err(err)) => err.len(),
_ => 0,
})

View File

@ -5,7 +5,13 @@ use egui::{
};
use std::{sync::Arc, task::Poll, thread};
type Entry = Poll<Result<Arc<[u8]>, String>>;
#[derive(Clone)]
struct File {
bytes: Arc<[u8]>,
mime: Option<String>,
}
type Entry = Poll<Result<File, String>>;
#[derive(Default)]
pub struct FileLoader {
@ -13,9 +19,17 @@ pub struct FileLoader {
cache: Arc<Mutex<HashMap<String, Entry>>>,
}
impl FileLoader {
pub const ID: &str = egui::generate_loader_id!(FileLoader);
}
const PROTOCOL: &str = "file://";
impl BytesLoader for FileLoader {
fn id(&self) -> &str {
Self::ID
}
fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult {
// File loader only supports the `file` protocol.
let Some(path) = uri.strip_prefix(PROTOCOL) else {
@ -26,9 +40,10 @@ impl BytesLoader for FileLoader {
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 {
Poll::Ready(Ok(file)) => Ok(BytesPoll::Ready {
size: None,
bytes: Bytes::Shared(bytes),
bytes: Bytes::Shared(file.bytes),
mime: file.mime,
}),
Poll::Ready(Err(err)) => Err(LoadError::Custom(err)),
Poll::Pending => Ok(BytesPoll::Pending { size: None }),
@ -51,7 +66,12 @@ impl BytesLoader for FileLoader {
let uri = uri.to_owned();
move || {
let result = match std::fs::read(&path) {
Ok(bytes) => Ok(bytes.into()),
Ok(bytes) => Ok(File {
bytes: bytes.into(),
mime: mime_guess::from_path(&path)
.first_raw()
.map(|v| v.to_owned()),
}),
Err(err) => Err(err.to_string()),
};
let prev = cache.lock().insert(path, Poll::Ready(result));
@ -70,12 +90,18 @@ impl BytesLoader for FileLoader {
let _ = self.cache.lock().remove(uri);
}
fn forget_all(&self) {
self.cache.lock().clear();
}
fn byte_size(&self) -> usize {
self.cache
.lock()
.values()
.map(|entry| match entry {
Poll::Ready(Ok(bytes)) => bytes.len(),
Poll::Ready(Ok(file)) => {
file.bytes.len() + file.mime.as_ref().map_or(0, |m| m.len())
}
Poll::Ready(Err(err)) => err.len(),
_ => 0,
})

View File

@ -13,7 +13,11 @@ pub struct ImageCrateLoader {
cache: Mutex<HashMap<String, Entry>>,
}
fn is_supported(uri: &str) -> bool {
impl ImageCrateLoader {
pub const ID: &str = egui::generate_loader_id!(ImageCrateLoader);
}
fn is_supported_uri(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
@ -22,9 +26,23 @@ fn is_supported(uri: &str) -> bool {
ext != "svg"
}
fn is_unsupported_mime(mime: &str) -> bool {
mime.contains("svg")
}
impl ImageLoader for ImageCrateLoader {
fn id(&self) -> &str {
Self::ID
}
fn load(&self, ctx: &egui::Context, uri: &str, _: SizeHint) -> ImageLoadResult {
if !is_supported(uri) {
// three stages of guessing if we support loading the image:
// 1. URI extension
// 2. Mime from `BytesPoll::Ready`
// 3. image::guess_format
// (1)
if !is_supported_uri(uri) {
return Err(LoadError::NotSupported);
}
@ -36,7 +54,14 @@ impl ImageLoader for ImageCrateLoader {
}
} else {
match ctx.try_load_bytes(uri) {
Ok(BytesPoll::Ready { bytes, .. }) => {
Ok(BytesPoll::Ready { bytes, mime, .. }) => {
// (2 and 3)
if mime.as_deref().is_some_and(is_unsupported_mime)
|| image::guess_format(&bytes).is_err()
{
return Err(LoadError::NotSupported);
}
crate::log_trace!("started loading {uri:?}");
let result = crate::image::load_image_bytes(&bytes).map(Arc::new);
crate::log_trace!("finished loading {uri:?}");
@ -56,6 +81,10 @@ impl ImageLoader for ImageCrateLoader {
let _ = self.cache.lock().remove(uri);
}
fn forget_all(&self) {
self.cache.lock().clear();
}
fn byte_size(&self) -> usize {
self.cache
.lock()
@ -74,11 +103,11 @@ mod tests {
#[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"));
assert!(is_supported_uri("https://test.png"));
assert!(is_supported_uri("test.jpeg"));
assert!(is_supported_uri("http://test.gif"));
assert!(is_supported_uri("test.webp"));
assert!(is_supported_uri("file://test"));
assert!(!is_supported_uri("test.svg"));
}
}

View File

@ -13,6 +13,10 @@ pub struct SvgLoader {
cache: Mutex<HashMap<(String, SizeHint), Entry>>,
}
impl SvgLoader {
pub const ID: &str = egui::generate_loader_id!(SvgLoader);
}
fn is_supported(uri: &str) -> bool {
let Some(ext) = Path::new(uri).extension().and_then(|ext| ext.to_str()) else { return false };
@ -20,6 +24,10 @@ fn is_supported(uri: &str) -> bool {
}
impl ImageLoader for SvgLoader {
fn id(&self) -> &str {
Self::ID
}
fn load(&self, ctx: &egui::Context, uri: &str, size_hint: SizeHint) -> ImageLoadResult {
if !is_supported(uri) {
return Err(LoadError::NotSupported);
@ -39,7 +47,7 @@ impl ImageLoader for SvgLoader {
Ok(BytesPoll::Ready { bytes, .. }) => {
crate::log_trace!("started loading {uri:?}");
let fit_to = match size_hint {
SizeHint::Original => usvg::FitTo::Original,
SizeHint::Scale(factor) => usvg::FitTo::Zoom(factor.into_inner()),
SizeHint::Width(w) => usvg::FitTo::Width(w),
SizeHint::Height(h) => usvg::FitTo::Height(h),
SizeHint::Size(w, h) => usvg::FitTo::Size(w, h),
@ -63,6 +71,10 @@ impl ImageLoader for SvgLoader {
self.cache.lock().retain(|(u, _), _| u != uri);
}
fn forget_all(&self) {
self.cache.lock().clear();
}
fn byte_size(&self) -> usize {
self.cache
.lock()

View File

@ -1234,7 +1234,7 @@ impl PlotItem for PlotImage {
Rect::from_two_pos(left_top_screen, right_bottom_screen)
};
let screen_rotation = -*rotation as f32;
Image::new(*texture_id, image_screen_rect.size())
RawImage::new((*texture_id, image_screen_rect.size()))
.bg_fill(*bg_fill)
.tint(*tint)
.uv(*uv)

View File

@ -86,7 +86,10 @@ impl TextureHandle {
/// width x height
pub fn size(&self) -> [usize; 2] {
self.tex_mngr.read().meta(self.id).unwrap().size
self.tex_mngr
.read()
.meta(self.id)
.map_or([0, 0], |tex| tex.size)
}
/// width x height
@ -97,7 +100,10 @@ impl TextureHandle {
/// `width x height x bytes_per_pixel`
pub fn byte_size(&self) -> usize {
self.tex_mngr.read().meta(self.id).unwrap().bytes_used()
self.tex_mngr
.read()
.meta(self.id)
.map_or(0, |tex| tex.bytes_used())
}
/// width / height
@ -108,7 +114,10 @@ impl TextureHandle {
/// Debug-name.
pub fn name(&self) -> String {
self.tex_mngr.read().meta(self.id).unwrap().name.clone()
self.tex_mngr
.read()
.meta(self.id)
.map_or_else(|| "<none>".to_owned(), |tex| tex.name.clone())
}
}

View File

@ -8,8 +8,16 @@ use std::hash::{Hash, Hasher};
/// Possible types for `T` are `f32` and `f64`.
///
/// See also [`FloatOrd`].
#[derive(Clone, Copy)]
pub struct OrderedFloat<T>(T);
impl<T: Float + Copy> OrderedFloat<T> {
#[inline]
pub fn into_inner(self) -> T {
self.0
}
}
impl<T: Float> Eq for OrderedFloat<T> {}
impl<T: Float> PartialEq<Self> for OrderedFloat<T> {

View File

@ -1,7 +0,0 @@
Example how to download and show an image with eframe/egui.
```sh
cargo run -p download_image
```
![](screenshot.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

View File

@ -1,41 +0,0 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::egui;
fn main() -> Result<(), eframe::Error> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions::default();
eframe::run_native(
"Download and show an image with eframe/egui",
options,
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)]
struct MyApp;
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
let width = ui.available_width();
let half_height = ui.available_height() / 2.0;
ui.allocate_ui(egui::Vec2::new(width, half_height), |ui| {
ui.add(egui::Image2::from_uri(
"https://picsum.photos/seed/1.759706314/1024",
))
});
ui.allocate_ui(egui::Vec2::new(width, half_height), |ui| {
ui.add(egui::Image2::from_uri(
"https://this-is-hopefully-not-a-real-website.rs/image.png",
))
});
});
}
}

View File

@ -1,7 +1,7 @@
[package]
name = "download_image"
name = "images"
version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
authors = ["Jan Procházka <github.com/jprochazk>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.70"
@ -10,12 +10,14 @@ publish = false
[dependencies]
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 = [
"http",
"image",
"log",
"all-loaders",
"log",
] }
env_logger = "0.10"
image = { version = "0.24", default-features = false, features = ["jpeg"] }
image = { version = "0.24", default-features = false, features = [
"jpeg",
"png",
] }

View File

@ -0,0 +1,7 @@
Example showing how to show images with eframe/egui.
```sh
cargo run -p images
```
![](screenshot.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -0,0 +1,41 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::egui;
use eframe::epaint::vec2;
fn main() -> Result<(), eframe::Error> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions {
drag_and_drop_support: true,
..Default::default()
};
eframe::run_native(
"Image Viewer",
options,
Box::new(|cc| {
// The following call is needed to load images when using `ui.image` and `egui::Image`:
egui_extras::loaders::install(&cc.egui_ctx);
Box::<MyApp>::default()
}),
)
}
#[derive(Default)]
struct MyApp {}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::new([true, true]).show(ui, |ui| {
ui.add(
egui::Image::new(egui::include_image!("ferris.svg"))
.fit_to_fraction(vec2(1.0, 0.5)),
);
ui.add(
egui::Image::new("https://picsum.photos/seed/1.759706314/1024".into())
.rounding(egui::Rounding::same(10.0)),
);
});
});
}
}

View File

@ -1,17 +0,0 @@
[package]
name = "retained_image"
version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.70"
publish = false
[dependencies]
eframe = { path = "../../crates/eframe", features = [
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
] }
egui_extras = { path = "../../crates/egui_extras", features = ["image", "log"] }
env_logger = "0.10"
image = { version = "0.24", default-features = false, features = ["png"] }

View File

@ -1,7 +0,0 @@
Example how to show an image with eframe/egui.
```sh
cargo run -p retained_image
```
![](screenshot.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

View File

@ -1,84 +0,0 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::egui;
use egui_extras::RetainedImage;
fn main() -> Result<(), eframe::Error> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions {
initial_window_size: Some(egui::vec2(400.0, 1000.0)),
..Default::default()
};
eframe::run_native(
"Show an image with eframe/egui",
options,
Box::new(|_cc| Box::<MyApp>::default()),
)
}
struct MyApp {
image: RetainedImage,
rounding: f32,
tint: egui::Color32,
}
impl Default for MyApp {
fn default() -> Self {
Self {
// crab image is CC0, found on https://stocksnap.io/search/crab
image: RetainedImage::from_image_bytes("crab.png", include_bytes!("crab.png")).unwrap(),
rounding: 32.0,
tint: egui::Color32::from_rgb(100, 200, 200),
}
}
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let Self {
image,
rounding,
tint,
} = self;
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("This is an image:");
image.show(ui);
ui.add_space(32.0);
ui.heading("This is a tinted image with rounded corners:");
ui.add(
egui::Image::new(image.texture_id(ctx), image.size_vec2())
.tint(*tint)
.rounding(*rounding),
);
ui.horizontal(|ui| {
ui.label("Tint:");
egui::color_picker::color_edit_button_srgba(
ui,
tint,
egui::color_picker::Alpha::BlendOrAdditive,
);
ui.add_space(16.0);
ui.label("Rounding:");
ui.add(
egui::DragValue::new(rounding)
.speed(1.0)
.clamp_range(0.0..=0.5 * image.size_vec2().min_elem()),
);
});
ui.add_space(32.0);
ui.heading("This is an image you can click:");
ui.add(egui::ImageButton::new(
image.texture_id(ctx),
image.size_vec2(),
));
});
}
}

View File

@ -63,7 +63,7 @@ impl eframe::App for MyApp {
});
if let Some(texture) = self.texture.as_ref() {
ui.image(texture, ui.available_size());
ui.raw_image((texture.id(), ui.available_size()));
} else {
ui.spinner();
}

View File

@ -1,16 +0,0 @@
[package]
name = "svg"
version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.70"
publish = false
[dependencies]
eframe = { path = "../../crates/eframe", features = [
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
] }
egui_extras = { path = "../../crates/egui_extras", features = ["log", "svg"] }
env_logger = "0.10"

View File

@ -1,7 +0,0 @@
Example how to show an SVG image.
```sh
cargo run -p svg
```
![](screenshot.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

View File

@ -1,47 +0,0 @@
//! A good way of displaying an SVG image in egui.
//!
//! Requires the dependency `egui_extras` with the `svg` feature.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::egui;
fn main() -> Result<(), eframe::Error> {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions {
initial_window_size: Some(egui::vec2(1000.0, 700.0)),
..Default::default()
};
eframe::run_native(
"svg example",
options,
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;
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("SVG example");
ui.label("The SVG is rasterized and displayed as a texture.");
ui.separator();
let max_size = ui.available_size();
ui.add(
egui::Image2::from_static_bytes(
"ferris.svg",
include_bytes!("rustacean-flat-happy.svg"),
)
.size_hint(max_size),
);
});
}
}

View File

@ -1 +0,0 @@
Rust logo by Mozilla, from https://github.com/rust-lang/rust-artwork