1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-02 06:40:06 -04:00

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
47 changed files with 1563 additions and 770 deletions

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;