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:
BIN
crates/egui/assets/ferris.png
Normal file
BIN
crates/egui/assets/ferris.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
///
|
||||
/// ```
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user