Final polish for new image loading (#3328)
* add egui logo to widget gallery * improve "no image loaders" error message * rework static URIs to accept `Cow<'static>` * remove `RetainedImage` from `http_app` in `egui_demo_app` * hide `RetainedImage` from docs * use `ui.image`/`Image` over `RawImage` * remove last remanant of `RawImage` * remove unused doc link * add style option to disable image spinners * use `Into<Image>` instead of `Into<ImageSource>` to allow configuring the underlying image * propagate `image_options` through `ImageButton` * calculate image size properly in `Button` * properly calculate size in `ImageButton` * Update crates/egui/src/widgets/image.rs Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com> * improve no image loaders error message * add `size()` helper to `TexturePoll` * try get size from poll in `Button` * add `paint_at` to `Spinner` * use `Spinner::paint_at` and hover on image button response * `show_spinner` -> `show_loading_spinner` * avoid `allocate_ui` in `Image` when painting spinner * make icon smaller + remove old texture * add `load_and_calculate_size` + expose `paint_image_at` * update `egui_plot` to paint image in the right place * Add helpers for painting an ImageSource directly * Use max_size=INF as default * Use new API in WidgetGallery * Make egui_demo_app work by default * Remove Option from scale * Refactor ImageSize * Fix docstring * Small refactor --------- Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
parent
fc3bddd0cf
commit
67a3fcae38
|
|
@ -605,9 +605,8 @@ impl Renderer {
|
||||||
|
|
||||||
/// Get the WGPU texture and bind group associated to a texture that has been allocated by egui.
|
/// Get the WGPU texture and bind group associated to a texture that has been allocated by egui.
|
||||||
///
|
///
|
||||||
/// This could be used by custom paint hooks to render images that have been added through with
|
/// This could be used by custom paint hooks to render images that have been added through
|
||||||
/// [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html)
|
/// [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture).
|
||||||
/// or [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture).
|
|
||||||
pub fn texture(
|
pub fn texture(
|
||||||
&self,
|
&self,
|
||||||
id: &epaint::TextureId,
|
id: &epaint::TextureId,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#![warn(missing_docs)] // Let's keep `Context` well-documented.
|
#![warn(missing_docs)] // Let's keep `Context` well-documented.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::load::Bytes;
|
use crate::load::Bytes;
|
||||||
|
|
@ -1145,7 +1146,7 @@ impl Context {
|
||||||
/// });
|
/// });
|
||||||
///
|
///
|
||||||
/// // Show the image:
|
/// // Show the image:
|
||||||
/// ui.raw_image((texture.id(), texture.size_vec2()));
|
/// ui.image((texture.id(), texture.size_vec2()));
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -1691,14 +1692,14 @@ impl Context {
|
||||||
let mut size = vec2(w as f32, h as f32);
|
let mut size = vec2(w as f32, h as f32);
|
||||||
size *= (max_preview_size.x / size.x).min(1.0);
|
size *= (max_preview_size.x / size.x).min(1.0);
|
||||||
size *= (max_preview_size.y / size.y).min(1.0);
|
size *= (max_preview_size.y / size.y).min(1.0);
|
||||||
ui.raw_image(SizedTexture::new(texture_id, size))
|
ui.image(SizedTexture::new(texture_id, size))
|
||||||
.on_hover_ui(|ui| {
|
.on_hover_ui(|ui| {
|
||||||
// show larger on hover
|
// show larger on hover
|
||||||
let max_size = 0.5 * ui.ctx().screen_rect().size();
|
let max_size = 0.5 * ui.ctx().screen_rect().size();
|
||||||
let mut size = vec2(w as f32, h as f32);
|
let mut size = vec2(w as f32, h as f32);
|
||||||
size *= max_size.x / size.x.max(max_size.x);
|
size *= max_size.x / size.x.max(max_size.x);
|
||||||
size *= max_size.y / size.y.max(max_size.y);
|
size *= max_size.y / size.y.max(max_size.y);
|
||||||
ui.raw_image(SizedTexture::new(texture_id, size));
|
ui.image(SizedTexture::new(texture_id, size));
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.label(format!("{w} x {h}"));
|
ui.label(format!("{w} x {h}"));
|
||||||
|
|
@ -1911,8 +1912,8 @@ impl Context {
|
||||||
/// Associate some static bytes with a `uri`.
|
/// Associate some static bytes with a `uri`.
|
||||||
///
|
///
|
||||||
/// The same `uri` may be passed to [`Ui::image`] later to load the bytes as an image.
|
/// 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>) {
|
pub fn include_bytes(&self, uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) {
|
||||||
self.loaders().include.insert(uri, bytes.into());
|
self.loaders().include.insert(uri, bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the chain of bytes, image, or texture loaders
|
/// Returns `true` if the chain of bytes, image, or texture loaders
|
||||||
|
|
@ -2038,17 +2039,25 @@ impl Context {
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// This may fail with:
|
/// This may fail with:
|
||||||
|
/// - [`LoadError::NoImageLoaders`][no_image_loaders] if tbere are no registered image loaders.
|
||||||
/// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`.
|
/// - [`LoadError::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.
|
/// - [`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`!
|
/// ⚠ May deadlock if called from within an `ImageLoader`!
|
||||||
///
|
///
|
||||||
|
/// [no_image_loaders]: crate::load::LoadError::NoImageLoaders
|
||||||
/// [not_supported]: crate::load::LoadError::NotSupported
|
/// [not_supported]: crate::load::LoadError::NotSupported
|
||||||
/// [custom]: crate::load::LoadError::Custom
|
/// [custom]: crate::load::LoadError::Custom
|
||||||
pub fn try_load_image(&self, uri: &str, size_hint: load::SizeHint) -> load::ImageLoadResult {
|
pub fn try_load_image(&self, uri: &str, size_hint: load::SizeHint) -> load::ImageLoadResult {
|
||||||
crate::profile_function!();
|
crate::profile_function!();
|
||||||
|
|
||||||
for loader in self.loaders().image.lock().iter() {
|
let loaders = self.loaders();
|
||||||
|
let loaders = loaders.image.lock();
|
||||||
|
if loaders.is_empty() {
|
||||||
|
return Err(load::LoadError::NoImageLoaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
for loader in loaders.iter() {
|
||||||
match loader.load(self, uri, size_hint) {
|
match loader.load(self, uri, size_hint) {
|
||||||
Err(load::LoadError::NotSupported) => continue,
|
Err(load::LoadError::NotSupported) => continue,
|
||||||
result => return result,
|
result => return result,
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@
|
||||||
//! ui.separator();
|
//! ui.separator();
|
||||||
//!
|
//!
|
||||||
//! # let my_image = egui::TextureId::default();
|
//! # let my_image = egui::TextureId::default();
|
||||||
//! ui.raw_image((my_image, egui::Vec2::new(640.0, 480.0)));
|
//! ui.image((my_image, egui::Vec2::new(640.0, 480.0)));
|
||||||
//!
|
//!
|
||||||
//! ui.collapsing("Click to see what is hidden!", |ui| {
|
//! ui.collapsing("Click to see what is hidden!", |ui| {
|
||||||
//! ui.label("Not much, as it turns out");
|
//! ui.label("Not much, as it turns out");
|
||||||
|
|
@ -442,7 +442,10 @@ pub fn warn_if_debug_build(ui: &mut crate::Ui) {
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! include_image {
|
macro_rules! include_image {
|
||||||
($path: literal) => {
|
($path: literal) => {
|
||||||
$crate::ImageSource::Bytes($path, $crate::load::Bytes::Static(include_bytes!($path)))
|
$crate::ImageSource::Bytes(
|
||||||
|
::std::borrow::Cow::Borrowed($path),
|
||||||
|
$crate::load::Bytes::Static(include_bytes!($path)),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,11 @@
|
||||||
//! For example, a loader may determine that it doesn't support loading a specific URI
|
//! For example, a loader may determine that it doesn't support loading a specific URI
|
||||||
//! if the protocol does not match what it expects.
|
//! if the protocol does not match what it expects.
|
||||||
|
|
||||||
|
mod bytes_loader;
|
||||||
|
mod texture_loader;
|
||||||
|
|
||||||
|
use self::bytes_loader::DefaultBytesLoader;
|
||||||
|
use self::texture_loader::DefaultTextureLoader;
|
||||||
use crate::Context;
|
use crate::Context;
|
||||||
use ahash::HashMap;
|
use ahash::HashMap;
|
||||||
use epaint::mutex::Mutex;
|
use epaint::mutex::Mutex;
|
||||||
|
|
@ -59,6 +64,7 @@ use epaint::util::FloatOrd;
|
||||||
use epaint::util::OrderedFloat;
|
use epaint::util::OrderedFloat;
|
||||||
use epaint::TextureHandle;
|
use epaint::TextureHandle;
|
||||||
use epaint::{textures::TextureOptions, ColorImage, TextureId, Vec2};
|
use epaint::{textures::TextureOptions, ColorImage, TextureId, Vec2};
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::{error::Error as StdError, fmt::Display, sync::Arc};
|
use std::{error::Error as StdError, fmt::Display, sync::Arc};
|
||||||
|
|
@ -66,6 +72,9 @@ use std::{error::Error as StdError, fmt::Display, sync::Arc};
|
||||||
/// Represents a failed attempt at loading an image.
|
/// Represents a failed attempt at loading an image.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum LoadError {
|
pub enum LoadError {
|
||||||
|
/// There are no image loaders installed.
|
||||||
|
NoImageLoaders,
|
||||||
|
|
||||||
/// This loader does not support this protocol or image format.
|
/// This loader does not support this protocol or image format.
|
||||||
NotSupported,
|
NotSupported,
|
||||||
|
|
||||||
|
|
@ -76,6 +85,9 @@ pub enum LoadError {
|
||||||
impl Display for LoadError {
|
impl Display for LoadError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
LoadError::NoImageLoaders => f.write_str(
|
||||||
|
"No image loaders are installed. If you're trying to load some images \
|
||||||
|
for the first time, follow the steps outlined in https://docs.rs/egui/latest/egui/load/index.html"),
|
||||||
LoadError::NotSupported => f.write_str("not supported"),
|
LoadError::NotSupported => f.write_str("not supported"),
|
||||||
LoadError::Custom(message) => f.write_str(message),
|
LoadError::Custom(message) => f.write_str(message),
|
||||||
}
|
}
|
||||||
|
|
@ -342,7 +354,7 @@ pub trait ImageLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A texture with a known size.
|
/// A texture with a known size.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub struct SizedTexture {
|
pub struct SizedTexture {
|
||||||
pub id: TextureId,
|
pub id: TextureId,
|
||||||
pub size: Vec2,
|
pub size: Vec2,
|
||||||
|
|
@ -370,7 +382,13 @@ impl SizedTexture {
|
||||||
impl From<(TextureId, Vec2)> for SizedTexture {
|
impl From<(TextureId, Vec2)> for SizedTexture {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from((id, size): (TextureId, Vec2)) -> Self {
|
fn from((id, size): (TextureId, Vec2)) -> Self {
|
||||||
SizedTexture { id, size }
|
Self { id, size }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a TextureHandle> for SizedTexture {
|
||||||
|
fn from(handle: &'a TextureHandle) -> Self {
|
||||||
|
Self::from_handle(handle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -379,7 +397,7 @@ impl From<(TextureId, Vec2)> for SizedTexture {
|
||||||
/// This is similar to [`std::task::Poll`], but the `Pending` variant
|
/// This is similar to [`std::task::Poll`], but the `Pending` variant
|
||||||
/// contains an optional `size`, which may be used during layout to
|
/// contains an optional `size`, which may be used during layout to
|
||||||
/// pre-allocate space the image.
|
/// pre-allocate space the image.
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum TexturePoll {
|
pub enum TexturePoll {
|
||||||
/// Texture is loading.
|
/// Texture is loading.
|
||||||
Pending {
|
Pending {
|
||||||
|
|
@ -391,6 +409,15 @@ pub enum TexturePoll {
|
||||||
Ready { texture: SizedTexture },
|
Ready { texture: SizedTexture },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TexturePoll {
|
||||||
|
pub fn size(self) -> Option<Vec2> {
|
||||||
|
match self {
|
||||||
|
TexturePoll::Pending { size } => size,
|
||||||
|
TexturePoll::Ready { texture } => Some(texture.size),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub type TextureLoadResult = Result<TexturePoll>;
|
pub type TextureLoadResult = Result<TexturePoll>;
|
||||||
|
|
||||||
/// Represents a loader capable of loading a full texture.
|
/// Represents a loader capable of loading a full texture.
|
||||||
|
|
@ -447,99 +474,6 @@ pub trait TextureLoader {
|
||||||
fn byte_size(&self) -> usize;
|
fn byte_size(&self) -> usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub(crate) struct DefaultBytesLoader {
|
|
||||||
cache: Mutex<HashMap<&'static str, Bytes>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DefaultBytesLoader {
|
|
||||||
pub(crate) fn insert(&self, uri: &'static str, bytes: impl Into<Bytes>) {
|
|
||||||
self.cache.lock().entry(uri).or_insert_with(|| bytes.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BytesLoader for DefaultBytesLoader {
|
|
||||||
fn id(&self) -> &str {
|
|
||||||
generate_loader_id!(DefaultBytesLoader)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load(&self, _: &Context, uri: &str) -> BytesLoadResult {
|
|
||||||
match self.cache.lock().get(uri).cloned() {
|
|
||||||
Some(bytes) => Ok(BytesPoll::Ready {
|
|
||||||
size: None,
|
|
||||||
bytes,
|
|
||||||
mime: None,
|
|
||||||
}),
|
|
||||||
None => Err(LoadError::NotSupported),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn forget(&self, uri: &str) {
|
|
||||||
let _ = self.cache.lock().remove(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn forget_all(&self) {
|
|
||||||
self.cache.lock().clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn byte_size(&self) -> usize {
|
|
||||||
self.cache.lock().values().map(|bytes| bytes.len()).sum()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct DefaultTextureLoader {
|
|
||||||
cache: Mutex<HashMap<(String, TextureOptions), TextureHandle>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TextureLoader for DefaultTextureLoader {
|
|
||||||
fn id(&self) -> &str {
|
|
||||||
generate_loader_id!(DefaultTextureLoader)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load(
|
|
||||||
&self,
|
|
||||||
ctx: &Context,
|
|
||||||
uri: &str,
|
|
||||||
texture_options: TextureOptions,
|
|
||||||
size_hint: SizeHint,
|
|
||||||
) -> TextureLoadResult {
|
|
||||||
let mut cache = self.cache.lock();
|
|
||||||
if let Some(handle) = cache.get(&(uri.into(), texture_options)) {
|
|
||||||
let texture = SizedTexture::from_handle(handle);
|
|
||||||
Ok(TexturePoll::Ready { texture })
|
|
||||||
} else {
|
|
||||||
match ctx.try_load_image(uri, size_hint)? {
|
|
||||||
ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }),
|
|
||||||
ImagePoll::Ready { image } => {
|
|
||||||
let handle = ctx.load_texture(uri, image, texture_options);
|
|
||||||
let texture = SizedTexture::from_handle(&handle);
|
|
||||||
cache.insert((uri.into(), texture_options), handle);
|
|
||||||
Ok(TexturePoll::Ready { texture })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn forget(&self, uri: &str) {
|
|
||||||
self.cache.lock().retain(|(u, _), _| u != uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn forget_all(&self) {
|
|
||||||
self.cache.lock().clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn end_frame(&self, _: usize) {}
|
|
||||||
|
|
||||||
fn byte_size(&self) -> usize {
|
|
||||||
self.cache
|
|
||||||
.lock()
|
|
||||||
.values()
|
|
||||||
.map(|texture| texture.byte_size())
|
|
||||||
.sum()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type BytesLoaderImpl = Arc<dyn BytesLoader + Send + Sync + 'static>;
|
type BytesLoaderImpl = Arc<dyn BytesLoader + Send + Sync + 'static>;
|
||||||
type ImageLoaderImpl = Arc<dyn ImageLoader + Send + Sync + 'static>;
|
type ImageLoaderImpl = Arc<dyn ImageLoader + Send + Sync + 'static>;
|
||||||
type TextureLoaderImpl = Arc<dyn TextureLoader + Send + Sync + 'static>;
|
type TextureLoaderImpl = Arc<dyn TextureLoader + Send + Sync + 'static>;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct DefaultBytesLoader {
|
||||||
|
cache: Mutex<HashMap<Cow<'static, str>, Bytes>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DefaultBytesLoader {
|
||||||
|
pub fn insert(&self, uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) {
|
||||||
|
self.cache
|
||||||
|
.lock()
|
||||||
|
.entry(uri.into())
|
||||||
|
.or_insert_with_key(|uri| {
|
||||||
|
let bytes: Bytes = bytes.into();
|
||||||
|
|
||||||
|
#[cfg(feature = "log")]
|
||||||
|
log::trace!("loaded {} bytes for uri {uri:?}", bytes.len());
|
||||||
|
|
||||||
|
bytes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BytesLoader for DefaultBytesLoader {
|
||||||
|
fn id(&self) -> &str {
|
||||||
|
generate_loader_id!(DefaultBytesLoader)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(&self, _: &Context, uri: &str) -> BytesLoadResult {
|
||||||
|
match self.cache.lock().get(uri).cloned() {
|
||||||
|
Some(bytes) => Ok(BytesPoll::Ready {
|
||||||
|
size: None,
|
||||||
|
bytes,
|
||||||
|
mime: None,
|
||||||
|
}),
|
||||||
|
None => Err(LoadError::NotSupported),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forget(&self, uri: &str) {
|
||||||
|
#[cfg(feature = "log")]
|
||||||
|
log::trace!("forget {uri:?}");
|
||||||
|
|
||||||
|
let _ = self.cache.lock().remove(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forget_all(&self) {
|
||||||
|
#[cfg(feature = "log")]
|
||||||
|
log::trace!("forget all");
|
||||||
|
|
||||||
|
self.cache.lock().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn byte_size(&self) -> usize {
|
||||||
|
self.cache.lock().values().map(|bytes| bytes.len()).sum()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct DefaultTextureLoader {
|
||||||
|
cache: Mutex<HashMap<(String, TextureOptions), TextureHandle>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextureLoader for DefaultTextureLoader {
|
||||||
|
fn id(&self) -> &str {
|
||||||
|
crate::generate_loader_id!(DefaultTextureLoader)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(
|
||||||
|
&self,
|
||||||
|
ctx: &Context,
|
||||||
|
uri: &str,
|
||||||
|
texture_options: TextureOptions,
|
||||||
|
size_hint: SizeHint,
|
||||||
|
) -> TextureLoadResult {
|
||||||
|
let mut cache = self.cache.lock();
|
||||||
|
if let Some(handle) = cache.get(&(uri.into(), texture_options)) {
|
||||||
|
let texture = SizedTexture::from_handle(handle);
|
||||||
|
Ok(TexturePoll::Ready { texture })
|
||||||
|
} else {
|
||||||
|
match ctx.try_load_image(uri, size_hint)? {
|
||||||
|
ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }),
|
||||||
|
ImagePoll::Ready { image } => {
|
||||||
|
let handle = ctx.load_texture(uri, image, texture_options);
|
||||||
|
let texture = SizedTexture::from_handle(&handle);
|
||||||
|
cache.insert((uri.into(), texture_options), handle);
|
||||||
|
Ok(TexturePoll::Ready { texture })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forget(&self, uri: &str) {
|
||||||
|
#[cfg(feature = "log")]
|
||||||
|
log::trace!("forget {uri:?}");
|
||||||
|
|
||||||
|
self.cache.lock().retain(|(u, _), _| u != uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forget_all(&self) {
|
||||||
|
#[cfg(feature = "log")]
|
||||||
|
log::trace!("forget all");
|
||||||
|
|
||||||
|
self.cache.lock().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_frame(&self, _: usize) {}
|
||||||
|
|
||||||
|
fn byte_size(&self) -> usize {
|
||||||
|
self.cache
|
||||||
|
.lock()
|
||||||
|
.values()
|
||||||
|
.map(|texture| texture.byte_size())
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -207,6 +207,9 @@ pub struct Style {
|
||||||
///
|
///
|
||||||
/// This only affects a few egui widgets.
|
/// This only affects a few egui widgets.
|
||||||
pub explanation_tooltips: bool,
|
pub explanation_tooltips: bool,
|
||||||
|
|
||||||
|
/// Show a spinner when loading an image.
|
||||||
|
pub image_loading_spinners: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Style {
|
impl Style {
|
||||||
|
|
@ -738,6 +741,7 @@ impl Default for Style {
|
||||||
animation_time: 1.0 / 12.0,
|
animation_time: 1.0 / 12.0,
|
||||||
debug: Default::default(),
|
debug: Default::default(),
|
||||||
explanation_tooltips: false,
|
explanation_tooltips: false,
|
||||||
|
image_loading_spinners: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -990,6 +994,7 @@ impl Style {
|
||||||
animation_time,
|
animation_time,
|
||||||
debug,
|
debug,
|
||||||
explanation_tooltips,
|
explanation_tooltips,
|
||||||
|
image_loading_spinners,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
visuals.light_dark_radio_buttons(ui);
|
visuals.light_dark_radio_buttons(ui);
|
||||||
|
|
@ -1057,6 +1062,9 @@ impl Style {
|
||||||
"Show explanatory text when hovering DragValue:s and other egui widgets",
|
"Show explanatory text when hovering DragValue:s and other egui widgets",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ui.checkbox(image_loading_spinners, "Image loading spinners")
|
||||||
|
.on_hover_text("Show a spinner when an Image is loading");
|
||||||
|
|
||||||
ui.vertical_centered(|ui| reset_button(ui, self));
|
ui.vertical_centered(|ui| reset_button(ui, self));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use epaint::mutex::RwLock;
|
use epaint::mutex::RwLock;
|
||||||
|
|
||||||
use crate::load::SizedTexture;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
containers::*, ecolor::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer,
|
containers::*, ecolor::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer,
|
||||||
util::IdTypeMap, widgets::*, *,
|
util::IdTypeMap, widgets::*, *,
|
||||||
|
|
@ -1582,47 +1581,10 @@ impl Ui {
|
||||||
/// from a file with a statically known path, unless you really want to
|
/// from a file with a statically known path, unless you really want to
|
||||||
/// load it at runtime instead!
|
/// load it at runtime instead!
|
||||||
///
|
///
|
||||||
/// See also [`crate::Image`], [`crate::ImageSource`] and [`Self::raw_image`].
|
/// See also [`crate::Image`], [`crate::ImageSource`].
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn image<'a>(&mut self, source: impl Into<ImageSource<'a>>) -> Response {
|
pub fn image<'a>(&mut self, source: impl Into<ImageSource<'a>>) -> Response {
|
||||||
Image::new(source.into()).ui(self)
|
Image::new(source).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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2236,13 +2198,13 @@ impl Ui {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn menu_image_button<'a, R>(
|
pub fn menu_image_button<'a, R>(
|
||||||
&mut self,
|
&mut self,
|
||||||
image_source: impl Into<ImageSource<'a>>,
|
image: impl Into<Image<'a>>,
|
||||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||||
) -> InnerResponse<Option<R>> {
|
) -> InnerResponse<Option<R>> {
|
||||||
if let Some(menu_state) = self.menu_state.clone() {
|
if let Some(menu_state) = self.menu_state.clone() {
|
||||||
menu::submenu_button(self, menu_state, String::new(), add_contents)
|
menu::submenu_button(self, menu_state, String::new(), add_contents)
|
||||||
} else {
|
} else {
|
||||||
menu::menu_image_button(self, ImageButton::new(image_source), add_contents)
|
menu::menu_image_button(self, ImageButton::new(image), add_contents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ use crate::*;
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
||||||
pub struct Button {
|
pub struct Button<'a> {
|
||||||
text: WidgetText,
|
text: WidgetText,
|
||||||
shortcut_text: WidgetText,
|
shortcut_text: WidgetText,
|
||||||
wrap: Option<bool>,
|
wrap: Option<bool>,
|
||||||
|
|
@ -34,10 +34,10 @@ pub struct Button {
|
||||||
frame: Option<bool>,
|
frame: Option<bool>,
|
||||||
min_size: Vec2,
|
min_size: Vec2,
|
||||||
rounding: Option<Rounding>,
|
rounding: Option<Rounding>,
|
||||||
image: Option<widgets::RawImage>,
|
image: Option<Image<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Button {
|
impl<'a> Button<'a> {
|
||||||
pub fn new(text: impl Into<WidgetText>) -> Self {
|
pub fn new(text: impl Into<WidgetText>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
text: text.into(),
|
text: text.into(),
|
||||||
|
|
@ -56,16 +56,9 @@ impl Button {
|
||||||
|
|
||||||
/// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the provided size.
|
/// Creates a button with an image to the left of the text. The size of the image as displayed is defined by the provided size.
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
pub fn image_and_text(
|
pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
|
||||||
texture_id: TextureId,
|
|
||||||
image_size: impl Into<Vec2>,
|
|
||||||
text: impl Into<WidgetText>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
image: Some(widgets::RawImage::new(SizedTexture {
|
image: Some(image.into()),
|
||||||
id: texture_id,
|
|
||||||
size: image_size.into(),
|
|
||||||
})),
|
|
||||||
..Self::new(text)
|
..Self::new(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -142,7 +135,7 @@ impl Button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Button {
|
impl Widget for Button<'_> {
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
let Button {
|
let Button {
|
||||||
text,
|
text,
|
||||||
|
|
@ -158,6 +151,11 @@ impl Widget for Button {
|
||||||
image,
|
image,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
|
let image_size = image
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|image| image.load_and_calculate_size(ui, ui.available_size()))
|
||||||
|
.unwrap_or(Vec2::ZERO);
|
||||||
|
|
||||||
let frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
|
let frame = frame.unwrap_or_else(|| ui.visuals().button_frame);
|
||||||
|
|
||||||
let mut button_padding = ui.spacing().button_padding;
|
let mut button_padding = ui.spacing().button_padding;
|
||||||
|
|
@ -166,8 +164,8 @@ impl Widget for Button {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x;
|
let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x;
|
||||||
if let Some(image) = &image {
|
if image.is_some() {
|
||||||
text_wrap_width -= image.size().x + ui.spacing().icon_spacing;
|
text_wrap_width -= image_size.x + ui.spacing().icon_spacing;
|
||||||
}
|
}
|
||||||
if !shortcut_text.is_empty() {
|
if !shortcut_text.is_empty() {
|
||||||
text_wrap_width -= 60.0; // Some space for the shortcut text (which we never wrap).
|
text_wrap_width -= 60.0; // Some space for the shortcut text (which we never wrap).
|
||||||
|
|
@ -178,9 +176,9 @@ impl Widget for Button {
|
||||||
.then(|| shortcut_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button));
|
.then(|| shortcut_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button));
|
||||||
|
|
||||||
let mut desired_size = text.size();
|
let mut desired_size = text.size();
|
||||||
if let Some(image) = &image {
|
if image.is_some() {
|
||||||
desired_size.x += image.size().x + ui.spacing().icon_spacing;
|
desired_size.x += image_size.x + ui.spacing().icon_spacing;
|
||||||
desired_size.y = desired_size.y.max(image.size().y);
|
desired_size.y = desired_size.y.max(image_size.y);
|
||||||
}
|
}
|
||||||
if let Some(shortcut_text) = &shortcut_text {
|
if let Some(shortcut_text) = &shortcut_text {
|
||||||
desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x;
|
desired_size.x += ui.spacing().item_spacing.x + shortcut_text.size().x;
|
||||||
|
|
@ -192,7 +190,7 @@ impl Widget for Button {
|
||||||
}
|
}
|
||||||
desired_size = desired_size.at_least(min_size);
|
desired_size = desired_size.at_least(min_size);
|
||||||
|
|
||||||
let (rect, response) = ui.allocate_at_least(desired_size, sense);
|
let (rect, mut response) = ui.allocate_at_least(desired_size, sense);
|
||||||
response.widget_info(|| WidgetInfo::labeled(WidgetType::Button, text.text()));
|
response.widget_info(|| WidgetInfo::labeled(WidgetType::Button, text.text()));
|
||||||
|
|
||||||
if ui.is_rect_visible(rect) {
|
if ui.is_rect_visible(rect) {
|
||||||
|
|
@ -206,10 +204,10 @@ impl Widget for Button {
|
||||||
.rect(rect.expand(visuals.expansion), rounding, fill, stroke);
|
.rect(rect.expand(visuals.expansion), rounding, fill, stroke);
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_pos = if let Some(image) = &image {
|
let text_pos = if image.is_some() {
|
||||||
let icon_spacing = ui.spacing().icon_spacing;
|
let icon_spacing = ui.spacing().icon_spacing;
|
||||||
pos2(
|
pos2(
|
||||||
rect.min.x + button_padding.x + image.size().x + icon_spacing,
|
rect.min.x + button_padding.x + image_size.x + icon_spacing,
|
||||||
rect.center().y - 0.5 * text.size().y,
|
rect.center().y - 0.5 * text.size().y,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -235,11 +233,23 @@ impl Widget for Button {
|
||||||
let image_rect = Rect::from_min_size(
|
let image_rect = Rect::from_min_size(
|
||||||
pos2(
|
pos2(
|
||||||
rect.min.x + button_padding.x,
|
rect.min.x + button_padding.x,
|
||||||
rect.center().y - 0.5 - (image.size().y / 2.0),
|
rect.center().y - 0.5 - (image_size.y / 2.0),
|
||||||
),
|
),
|
||||||
image.size(),
|
image_size,
|
||||||
);
|
);
|
||||||
image.paint_at(ui, image_rect);
|
let tlr = image.load(ui);
|
||||||
|
let show_loading_spinner = image
|
||||||
|
.show_loading_spinner
|
||||||
|
.unwrap_or(ui.style().image_loading_spinners);
|
||||||
|
widgets::image::paint_texture_load_result(
|
||||||
|
ui,
|
||||||
|
&tlr,
|
||||||
|
image_rect,
|
||||||
|
show_loading_spinner,
|
||||||
|
image.image_options(),
|
||||||
|
);
|
||||||
|
response =
|
||||||
|
widgets::image::texture_load_result_response(image.source(), &tlr, response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -481,9 +491,9 @@ pub struct ImageButton<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ImageButton<'a> {
|
impl<'a> ImageButton<'a> {
|
||||||
pub fn new(source: impl Into<ImageSource<'a>>) -> Self {
|
pub fn new(image: impl Into<Image<'a>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
image: Image::new(source.into()),
|
image: image.into(),
|
||||||
sense: Sense::click(),
|
sense: Sense::click(),
|
||||||
frame: true,
|
frame: true,
|
||||||
selected: false,
|
selected: false,
|
||||||
|
|
@ -564,9 +574,9 @@ impl<'a> ImageButton<'a> {
|
||||||
.align_size_within_rect(texture.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
|
// let image_rect = image_rect.expand2(expansion); // can make it blurry, so let's not
|
||||||
let image_options = ImageOptions {
|
let image_options = ImageOptions {
|
||||||
rounding,
|
rounding, // apply rounding to the image
|
||||||
..Default::default()
|
..self.image.image_options().clone()
|
||||||
}; // apply rounding to the image
|
};
|
||||||
crate::widgets::image::paint_image_at(ui, image_rect, &image_options, texture);
|
crate::widgets::image::paint_image_at(ui, image_rect, &image_options, texture);
|
||||||
|
|
||||||
// Draw frame outline:
|
// Draw frame outline:
|
||||||
|
|
@ -581,7 +591,10 @@ impl<'a> ImageButton<'a> {
|
||||||
impl<'a> Widget for ImageButton<'a> {
|
impl<'a> Widget for ImageButton<'a> {
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
match self.image.load(ui) {
|
match self.image.load(ui) {
|
||||||
Ok(TexturePoll::Ready { texture }) => self.show(ui, &texture),
|
Ok(TexturePoll::Ready { mut texture }) => {
|
||||||
|
texture.size = self.image.calculate_size(ui.available_size(), texture.size);
|
||||||
|
self.show(ui, &texture)
|
||||||
|
}
|
||||||
Ok(TexturePoll::Pending { .. }) => ui
|
Ok(TexturePoll::Pending { .. }) => ui
|
||||||
.spinner()
|
.spinner()
|
||||||
.on_hover_text(format!("Loading {:?}…", self.image.uri())),
|
.on_hover_text(format!("Loading {:?}…", self.image.uri())),
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,6 @@ use epaint::{util::FloatOrd, RectShape};
|
||||||
/// - [`ImageSource::Bytes`] will also load the image using the [asynchronous loading process][`load`], but with lower latency.
|
/// - [`ImageSource::Bytes`] will also load the image using the [asynchronous loading process][`load`], but with lower latency.
|
||||||
/// - [`ImageSource::Texture`] will use the provided texture.
|
/// - [`ImageSource::Texture`] will use the provided texture.
|
||||||
///
|
///
|
||||||
/// To use a texture you already have with a simpler API, consider using [`RawImage`].
|
|
||||||
///
|
|
||||||
/// See [`load`] for more information.
|
/// See [`load`] for more information.
|
||||||
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -28,18 +26,36 @@ pub struct Image<'a> {
|
||||||
image_options: ImageOptions,
|
image_options: ImageOptions,
|
||||||
sense: Sense,
|
sense: Sense,
|
||||||
size: ImageSize,
|
size: ImageSize,
|
||||||
|
pub(crate) show_loading_spinner: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Image<'a> {
|
impl<'a> Image<'a> {
|
||||||
/// Load the image from some source.
|
/// Load the image from some source.
|
||||||
pub fn new(source: ImageSource<'a>) -> Self {
|
pub fn new(source: impl Into<ImageSource<'a>>) -> Self {
|
||||||
Self {
|
fn new_mono(source: ImageSource<'_>) -> Image<'_> {
|
||||||
source,
|
let size = if let ImageSource::Texture(tex) = &source {
|
||||||
texture_options: Default::default(),
|
// User is probably expecting their texture to have
|
||||||
image_options: Default::default(),
|
// the exact size of the provided `SizedTexture`.
|
||||||
sense: Sense::hover(),
|
ImageSize {
|
||||||
size: Default::default(),
|
maintain_aspect_ratio: true,
|
||||||
|
max_size: Vec2::INFINITY,
|
||||||
|
fit: ImageFit::Exact(tex.size),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
Image {
|
||||||
|
source,
|
||||||
|
texture_options: Default::default(),
|
||||||
|
image_options: Default::default(),
|
||||||
|
sense: Sense::hover(),
|
||||||
|
size,
|
||||||
|
show_loading_spinner: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
new_mono(source.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the image from a URI.
|
/// Load the image from a URI.
|
||||||
|
|
@ -52,15 +68,15 @@ impl<'a> Image<'a> {
|
||||||
/// Load the image from an existing texture.
|
/// Load the image from an existing texture.
|
||||||
///
|
///
|
||||||
/// See [`ImageSource::Texture`].
|
/// See [`ImageSource::Texture`].
|
||||||
pub fn from_texture(texture: SizedTexture) -> Self {
|
pub fn from_texture(texture: impl Into<SizedTexture>) -> Self {
|
||||||
Self::new(ImageSource::Texture(texture))
|
Self::new(ImageSource::Texture(texture.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the image from some raw bytes.
|
/// Load the image from some raw bytes.
|
||||||
///
|
///
|
||||||
/// See [`ImageSource::Bytes`].
|
/// See [`ImageSource::Bytes`].
|
||||||
pub fn from_bytes(uri: &'static str, bytes: impl Into<Bytes>) -> Self {
|
pub fn from_bytes(uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) -> Self {
|
||||||
Self::new(ImageSource::Bytes(uri, bytes.into()))
|
Self::new(ImageSource::Bytes(uri.into(), bytes.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Texture options used when creating the texture.
|
/// Texture options used when creating the texture.
|
||||||
|
|
@ -75,10 +91,7 @@ impl<'a> Image<'a> {
|
||||||
/// No matter what the image is scaled to, it will never exceed this limit.
|
/// No matter what the image is scaled to, it will never exceed this limit.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn max_width(mut self, width: f32) -> Self {
|
pub fn max_width(mut self, width: f32) -> Self {
|
||||||
match self.size.max_size.as_mut() {
|
self.size.max_size.x = width;
|
||||||
Some(max_size) => max_size.x = width,
|
|
||||||
None => self.size.max_size = Some(Vec2::new(width, f32::INFINITY)),
|
|
||||||
}
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,10 +100,7 @@ impl<'a> Image<'a> {
|
||||||
/// No matter what the image is scaled to, it will never exceed this limit.
|
/// No matter what the image is scaled to, it will never exceed this limit.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn max_height(mut self, height: f32) -> Self {
|
pub fn max_height(mut self, height: f32) -> Self {
|
||||||
match self.size.max_size.as_mut() {
|
self.size.max_size.y = height;
|
||||||
Some(max_size) => max_size.y = height,
|
|
||||||
None => self.size.max_size = Some(Vec2::new(f32::INFINITY, height)),
|
|
||||||
}
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,7 +108,7 @@ impl<'a> Image<'a> {
|
||||||
///
|
///
|
||||||
/// No matter what the image is scaled to, it will never exceed this limit.
|
/// No matter what the image is scaled to, it will never exceed this limit.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn max_size(mut self, size: Option<Vec2>) -> Self {
|
pub fn max_size(mut self, size: Vec2) -> Self {
|
||||||
self.size.max_size = size;
|
self.size.max_size = size;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -110,14 +120,14 @@ impl<'a> Image<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fit the image to its original size.
|
/// Fit the image to its original size with some scaling.
|
||||||
///
|
///
|
||||||
/// This will cause the image to overflow if it is larger than the available space.
|
/// 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.
|
/// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn fit_to_original_size(mut self, scale: Option<f32>) -> Self {
|
pub fn fit_to_original_size(mut self, scale: f32) -> Self {
|
||||||
self.size.fit = ImageFit::Original(scale);
|
self.size.fit = ImageFit::Original { scale };
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,18 +167,21 @@ impl<'a> Image<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
|
/// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
|
||||||
|
#[inline]
|
||||||
pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
|
pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
|
||||||
self.image_options.uv = uv.into();
|
self.image_options.uv = uv.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A solid color to put behind the image. Useful for transparent images.
|
/// A solid color to put behind the image. Useful for transparent images.
|
||||||
|
#[inline]
|
||||||
pub fn bg_fill(mut self, bg_fill: impl Into<Color32>) -> Self {
|
pub fn bg_fill(mut self, bg_fill: impl Into<Color32>) -> Self {
|
||||||
self.image_options.bg_fill = bg_fill.into();
|
self.image_options.bg_fill = bg_fill.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Multiply image color with this. Default is WHITE (no tint).
|
/// Multiply image color with this. Default is WHITE (no tint).
|
||||||
|
#[inline]
|
||||||
pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
|
pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
|
||||||
self.image_options.tint = tint.into();
|
self.image_options.tint = tint.into();
|
||||||
self
|
self
|
||||||
|
|
@ -183,6 +196,7 @@ impl<'a> Image<'a> {
|
||||||
///
|
///
|
||||||
/// Due to limitations in the current implementation,
|
/// Due to limitations in the current implementation,
|
||||||
/// this will turn off rounding of the image.
|
/// this will turn off rounding of the image.
|
||||||
|
#[inline]
|
||||||
pub fn rotate(mut self, angle: f32, origin: Vec2) -> Self {
|
pub fn rotate(mut self, angle: f32, origin: Vec2) -> Self {
|
||||||
self.image_options.rotation = Some((Rot2::from_angle(angle), origin));
|
self.image_options.rotation = Some((Rot2::from_angle(angle), origin));
|
||||||
self.image_options.rounding = Rounding::ZERO; // incompatible with rotation
|
self.image_options.rounding = Rounding::ZERO; // incompatible with rotation
|
||||||
|
|
@ -195,6 +209,7 @@ impl<'a> Image<'a> {
|
||||||
///
|
///
|
||||||
/// Due to limitations in the current implementation,
|
/// Due to limitations in the current implementation,
|
||||||
/// this will turn off any rotation of the image.
|
/// this will turn off any rotation of the image.
|
||||||
|
#[inline]
|
||||||
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
|
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
|
||||||
self.image_options.rounding = rounding.into();
|
self.image_options.rounding = rounding.into();
|
||||||
if self.image_options.rounding != Rounding::ZERO {
|
if self.image_options.rounding != Rounding::ZERO {
|
||||||
|
|
@ -202,14 +217,36 @@ impl<'a> Image<'a> {
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show a spinner when the image is loading.
|
||||||
|
///
|
||||||
|
/// By default this uses the value of [`Style::image_loading_spinners`].
|
||||||
|
#[inline]
|
||||||
|
pub fn show_loading_spinner(mut self, show: bool) -> Self {
|
||||||
|
self.show_loading_spinner = Some(show);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Into<ImageSource<'a>>> From<T> for Image<'a> {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Image::new(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Image<'a> {
|
impl<'a> Image<'a> {
|
||||||
/// Returns the size the image will occupy in the final UI.
|
/// Returns the size the image will occupy in the final UI.
|
||||||
|
#[inline]
|
||||||
pub fn calculate_size(&self, available_size: Vec2, image_size: Vec2) -> Vec2 {
|
pub fn calculate_size(&self, available_size: Vec2, image_size: Vec2) -> Vec2 {
|
||||||
self.size.get(available_size, image_size)
|
self.size.get(available_size, image_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn load_and_calculate_size(&self, ui: &mut Ui, available_size: Vec2) -> Option<Vec2> {
|
||||||
|
let image_size = self.load(ui).ok()?.size()?;
|
||||||
|
Some(self.size.get(available_size, image_size))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn size(&self) -> Option<Vec2> {
|
pub fn size(&self) -> Option<Vec2> {
|
||||||
match &self.source {
|
match &self.source {
|
||||||
ImageSource::Texture(texture) => Some(texture.size),
|
ImageSource::Texture(texture) => Some(texture.size),
|
||||||
|
|
@ -217,6 +254,12 @@ impl<'a> Image<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn image_options(&self) -> &ImageOptions {
|
||||||
|
&self.image_options
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn source(&self) -> &ImageSource<'a> {
|
pub fn source(&self) -> &ImageSource<'a> {
|
||||||
&self.source
|
&self.source
|
||||||
}
|
}
|
||||||
|
|
@ -224,13 +267,9 @@ impl<'a> Image<'a> {
|
||||||
/// Get the `uri` that this image was constructed from.
|
/// Get the `uri` that this image was constructed from.
|
||||||
///
|
///
|
||||||
/// This will return `<unknown>` for [`ImageSource::Texture`].
|
/// This will return `<unknown>` for [`ImageSource::Texture`].
|
||||||
|
#[inline]
|
||||||
pub fn uri(&self) -> &str {
|
pub fn uri(&self) -> &str {
|
||||||
match &self.source {
|
self.source.uri().unwrap_or("<unknown>")
|
||||||
ImageSource::Bytes(uri, _) => uri,
|
|
||||||
ImageSource::Uri(uri) => uri,
|
|
||||||
// Note: texture source is never in "loading" state
|
|
||||||
ImageSource::Texture(_) => "<unknown>",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the image from its [`Image::source`], returning the resulting [`SizedTexture`].
|
/// Load the image from its [`Image::source`], returning the resulting [`SizedTexture`].
|
||||||
|
|
@ -239,24 +278,13 @@ impl<'a> Image<'a> {
|
||||||
///
|
///
|
||||||
/// May fail if they underlying [`Context::try_load_texture`] call fails.
|
/// May fail if they underlying [`Context::try_load_texture`] call fails.
|
||||||
pub fn load(&self, ui: &Ui) -> TextureLoadResult {
|
pub fn load(&self, ui: &Ui) -> TextureLoadResult {
|
||||||
match self.source.clone() {
|
let size_hint = self.size.hint(ui.available_size());
|
||||||
ImageSource::Texture(texture) => Ok(TexturePoll::Ready { texture }),
|
self.source
|
||||||
ImageSource::Uri(uri) => ui.ctx().try_load_texture(
|
.clone()
|
||||||
uri.as_ref(),
|
.load(ui.ctx(), self.texture_options, size_hint)
|
||||||
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()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn paint_at(&self, ui: &mut Ui, rect: Rect, texture: &SizedTexture) {
|
pub fn paint_at(&self, ui: &mut Ui, rect: Rect, texture: &SizedTexture) {
|
||||||
paint_image_at(ui, rect, &self.image_options, texture);
|
paint_image_at(ui, rect, &self.image_options, texture);
|
||||||
}
|
}
|
||||||
|
|
@ -265,27 +293,28 @@ impl<'a> Image<'a> {
|
||||||
impl<'a> Widget for Image<'a> {
|
impl<'a> Widget for Image<'a> {
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
match self.load(ui) {
|
match self.load(ui) {
|
||||||
Ok(TexturePoll::Ready { texture }) => {
|
Ok(texture_poll) => {
|
||||||
let size = self.calculate_size(ui.available_size(), texture.size);
|
let texture_size = texture_poll.size();
|
||||||
let (rect, response) = ui.allocate_exact_size(size, self.sense);
|
let texture_size =
|
||||||
self.paint_at(ui, rect, &texture);
|
texture_size.unwrap_or_else(|| Vec2::splat(ui.style().spacing.interact_size.y));
|
||||||
response
|
let ui_size = self.calculate_size(ui.available_size(), texture_size);
|
||||||
}
|
let (rect, response) = ui.allocate_exact_size(ui_size, self.sense);
|
||||||
Ok(TexturePoll::Pending { size }) => match size {
|
match texture_poll {
|
||||||
Some(size) => {
|
TexturePoll::Ready { texture } => {
|
||||||
let size = self.calculate_size(ui.available_size(), size);
|
self.paint_at(ui, rect, &texture);
|
||||||
ui.allocate_ui(size, |ui| {
|
response
|
||||||
ui.with_layout(Layout::centered_and_justified(Direction::TopDown), |ui| {
|
}
|
||||||
ui.spinner()
|
TexturePoll::Pending { .. } => {
|
||||||
.on_hover_text(format!("Loading {:?}…", self.uri()))
|
let show_spinner = self
|
||||||
})
|
.show_loading_spinner
|
||||||
})
|
.unwrap_or(ui.style().image_loading_spinners);
|
||||||
.response
|
if show_spinner {
|
||||||
|
Spinner::new().paint_at(ui, response.rect);
|
||||||
|
}
|
||||||
|
response.on_hover_text(format!("Loading {:?}…", self.uri()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None => ui
|
}
|
||||||
.spinner()
|
|
||||||
.on_hover_text(format!("Loading {:?}…", self.uri())),
|
|
||||||
},
|
|
||||||
Err(err) => ui
|
Err(err) => ui
|
||||||
.colored_label(ui.visuals().error_fg_color, "⚠")
|
.colored_label(ui.visuals().error_fg_color, "⚠")
|
||||||
.on_hover_text(err.to_string()),
|
.on_hover_text(err.to_string()),
|
||||||
|
|
@ -306,8 +335,8 @@ pub struct ImageSize {
|
||||||
|
|
||||||
/// Determines the maximum size of the image.
|
/// Determines the maximum size of the image.
|
||||||
///
|
///
|
||||||
/// Defaults to `None`
|
/// Defaults to `Vec2::INFINITY` (no limit).
|
||||||
pub max_size: Option<Vec2>,
|
pub max_size: Vec2,
|
||||||
|
|
||||||
/// Determines how the image should shrink/expand/stretch/etc. to fit within its allocated space.
|
/// Determines how the image should shrink/expand/stretch/etc. to fit within its allocated space.
|
||||||
///
|
///
|
||||||
|
|
@ -323,8 +352,8 @@ pub struct ImageSize {
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub enum ImageFit {
|
pub enum ImageFit {
|
||||||
/// Fit the image to its original size, optionally scaling it by some factor.
|
/// Fit the image to its original size, scaled by some factor.
|
||||||
Original(Option<f32>),
|
Original { scale: f32 },
|
||||||
|
|
||||||
/// Fit the image to a fraction of the available size.
|
/// Fit the image to a fraction of the available size.
|
||||||
Fraction(Vec2),
|
Fraction(Vec2),
|
||||||
|
|
@ -333,6 +362,16 @@ pub enum ImageFit {
|
||||||
Exact(Vec2),
|
Exact(Vec2),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ImageFit {
|
||||||
|
pub fn resolve(self, available_size: Vec2, image_size: Vec2) -> Vec2 {
|
||||||
|
match self {
|
||||||
|
ImageFit::Original { scale } => image_size * scale,
|
||||||
|
ImageFit::Fraction(fract) => available_size * fract,
|
||||||
|
ImageFit::Exact(size) => size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ImageSize {
|
impl ImageSize {
|
||||||
fn hint(&self, available_size: Vec2) -> SizeHint {
|
fn hint(&self, available_size: Vec2) -> SizeHint {
|
||||||
if self.maintain_aspect_ratio {
|
if self.maintain_aspect_ratio {
|
||||||
|
|
@ -340,15 +379,12 @@ impl ImageSize {
|
||||||
};
|
};
|
||||||
|
|
||||||
let fit = match self.fit {
|
let fit = match self.fit {
|
||||||
ImageFit::Original(scale) => return SizeHint::Scale(scale.unwrap_or(1.0).ord()),
|
ImageFit::Original { scale } => return SizeHint::Scale(scale.ord()),
|
||||||
ImageFit::Fraction(fract) => available_size * fract,
|
ImageFit::Fraction(fract) => available_size * fract,
|
||||||
ImageFit::Exact(size) => size,
|
ImageFit::Exact(size) => size,
|
||||||
};
|
};
|
||||||
|
|
||||||
let fit = match self.max_size {
|
let fit = fit.min(self.max_size);
|
||||||
Some(extent) => fit.min(extent),
|
|
||||||
None => fit,
|
|
||||||
};
|
|
||||||
|
|
||||||
// `inf` on an axis means "any value"
|
// `inf` on an axis means "any value"
|
||||||
match (fit.x.is_finite(), fit.y.is_finite()) {
|
match (fit.x.is_finite(), fit.y.is_finite()) {
|
||||||
|
|
@ -360,74 +396,50 @@ impl ImageSize {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(&self, available_size: Vec2, image_size: Vec2) -> Vec2 {
|
fn get(&self, available_size: Vec2, image_size: Vec2) -> Vec2 {
|
||||||
match self.fit {
|
let Self {
|
||||||
ImageFit::Original(scale) => {
|
maintain_aspect_ratio,
|
||||||
let image_size = image_size * scale.unwrap_or(1.0);
|
max_size,
|
||||||
|
fit,
|
||||||
if let Some(available_size) = self.max_size {
|
} = *self;
|
||||||
if image_size.x < available_size.x && image_size.y < available_size.y {
|
match fit {
|
||||||
return image_size;
|
ImageFit::Original { scale } => {
|
||||||
}
|
let image_size = image_size * scale;
|
||||||
|
if image_size.x <= max_size.x && image_size.y <= max_size.y {
|
||||||
if self.maintain_aspect_ratio {
|
image_size
|
||||||
let ratio_x = available_size.x / image_size.x;
|
} else {
|
||||||
let ratio_y = available_size.y / image_size.y;
|
scale_to_fit(image_size, max_size, maintain_aspect_ratio)
|
||||||
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) => {
|
ImageFit::Fraction(fract) => {
|
||||||
let available_size = available_size * fract;
|
let scale_to_size = (available_size * fract).min(max_size);
|
||||||
let available_size = match self.max_size {
|
scale_to_fit(image_size, scale_to_size, maintain_aspect_ratio)
|
||||||
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) => {
|
ImageFit::Exact(size) => {
|
||||||
let available_size = size;
|
let scale_to_size = size.min(max_size);
|
||||||
let available_size = match self.max_size {
|
scale_to_fit(image_size, scale_to_size, maintain_aspect_ratio)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: unit-tests
|
||||||
|
fn scale_to_fit(image_size: Vec2, available_size: Vec2, maintain_aspect_ratio: bool) -> Vec2 {
|
||||||
|
if 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_finite() { ratio } else { 1.0 };
|
||||||
|
image_size * ratio
|
||||||
|
} else {
|
||||||
|
available_size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for ImageSize {
|
impl Default for ImageSize {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
max_size: None,
|
max_size: Vec2::INFINITY,
|
||||||
fit: ImageFit::Fraction(Vec2::new(1.0, 1.0)),
|
fit: ImageFit::Fraction(Vec2::new(1.0, 1.0)),
|
||||||
maintain_aspect_ratio: true,
|
maintain_aspect_ratio: true,
|
||||||
}
|
}
|
||||||
|
|
@ -452,8 +464,6 @@ pub enum ImageSource<'a> {
|
||||||
///
|
///
|
||||||
/// The user is responsible for loading the texture, determining its size,
|
/// The user is responsible for loading the texture, determining its size,
|
||||||
/// and allocating a [`TextureId`] for it.
|
/// and allocating a [`TextureId`] for it.
|
||||||
///
|
|
||||||
/// Note that a simpler API for this exists in [`RawImage`].
|
|
||||||
Texture(SizedTexture),
|
Texture(SizedTexture),
|
||||||
|
|
||||||
/// Load the image from some raw bytes.
|
/// Load the image from some raw bytes.
|
||||||
|
|
@ -467,7 +477,84 @@ pub enum ImageSource<'a> {
|
||||||
/// See also [`include_image`] for an easy way to load and display static images.
|
/// See also [`include_image`] for an easy way to load and display static images.
|
||||||
///
|
///
|
||||||
/// See [`crate::load`] for more information.
|
/// See [`crate::load`] for more information.
|
||||||
Bytes(&'static str, Bytes),
|
Bytes(Cow<'static, str>, Bytes),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ImageSource<'a> {
|
||||||
|
/// # Errors
|
||||||
|
/// Failure to load the texture.
|
||||||
|
pub fn load(
|
||||||
|
self,
|
||||||
|
ctx: &Context,
|
||||||
|
texture_options: TextureOptions,
|
||||||
|
size_hint: SizeHint,
|
||||||
|
) -> TextureLoadResult {
|
||||||
|
match self {
|
||||||
|
Self::Texture(texture) => Ok(TexturePoll::Ready { texture }),
|
||||||
|
Self::Uri(uri) => ctx.try_load_texture(uri.as_ref(), texture_options, size_hint),
|
||||||
|
Self::Bytes(uri, bytes) => {
|
||||||
|
ctx.include_bytes(uri.clone(), bytes);
|
||||||
|
ctx.try_load_texture(uri.as_ref(), texture_options, size_hint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the `uri` that this image was constructed from.
|
||||||
|
///
|
||||||
|
/// This will return `None` for [`Self::Texture`].
|
||||||
|
pub fn uri(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
ImageSource::Bytes(uri, _) | ImageSource::Uri(uri) => Some(uri),
|
||||||
|
ImageSource::Texture(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paint_texture_load_result(
|
||||||
|
ui: &Ui,
|
||||||
|
tlr: &TextureLoadResult,
|
||||||
|
rect: Rect,
|
||||||
|
show_loading_spinner: bool,
|
||||||
|
options: &ImageOptions,
|
||||||
|
) {
|
||||||
|
match tlr {
|
||||||
|
Ok(TexturePoll::Ready { texture }) => {
|
||||||
|
paint_image_at(ui, rect, options, texture);
|
||||||
|
}
|
||||||
|
Ok(TexturePoll::Pending { .. }) => {
|
||||||
|
if show_loading_spinner {
|
||||||
|
Spinner::new().paint_at(ui, rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
let font_id = TextStyle::Body.resolve(ui.style());
|
||||||
|
ui.painter().text(
|
||||||
|
rect.center(),
|
||||||
|
Align2::CENTER_CENTER,
|
||||||
|
"⚠",
|
||||||
|
font_id,
|
||||||
|
ui.visuals().error_fg_color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn texture_load_result_response(
|
||||||
|
source: &ImageSource<'_>,
|
||||||
|
tlr: &TextureLoadResult,
|
||||||
|
response: Response,
|
||||||
|
) -> Response {
|
||||||
|
match tlr {
|
||||||
|
Ok(TexturePoll::Ready { .. }) => response,
|
||||||
|
Ok(TexturePoll::Pending { .. }) => {
|
||||||
|
if let Some(uri) = source.uri() {
|
||||||
|
response.on_hover_text(format!("Loading {uri}…"))
|
||||||
|
} else {
|
||||||
|
response.on_hover_text("Loading image…")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => response.on_hover_text(err.to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a str> for ImageSource<'a> {
|
impl<'a> From<&'a str> for ImageSource<'a> {
|
||||||
|
|
@ -507,123 +594,30 @@ impl<'a> From<Cow<'a, str>> for ImageSource<'a> {
|
||||||
impl<T: Into<Bytes>> From<(&'static str, T)> for ImageSource<'static> {
|
impl<T: Into<Bytes>> From<(&'static str, T)> for ImageSource<'static> {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from((uri, bytes): (&'static str, T)) -> Self {
|
fn from((uri, bytes): (&'static str, T)) -> Self {
|
||||||
|
Self::Bytes(uri.into(), bytes.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Into<Bytes>> From<(Cow<'static, str>, T)> for ImageSource<'static> {
|
||||||
|
#[inline]
|
||||||
|
fn from((uri, bytes): (Cow<'static, str>, T)) -> Self {
|
||||||
Self::Bytes(uri, bytes.into())
|
Self::Bytes(uri, bytes.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: Into<Bytes>> From<(String, T)> for ImageSource<'static> {
|
||||||
|
#[inline]
|
||||||
|
fn from((uri, bytes): (String, T)) -> Self {
|
||||||
|
Self::Bytes(uri.into(), bytes.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: Into<SizedTexture>> From<T> for ImageSource<'static> {
|
impl<T: Into<SizedTexture>> From<T> for ImageSource<'static> {
|
||||||
fn from(value: T) -> Self {
|
fn from(value: T) -> Self {
|
||||||
Self::Texture(value.into())
|
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(texture: impl Into<SizedTexture>) -> Self {
|
|
||||||
Self {
|
|
||||||
texture: texture.into(),
|
|
||||||
texture_options: Default::default(),
|
|
||||||
image_options: Default::default(),
|
|
||||||
sense: Sense::hover(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Texture options used when creating the texture.
|
|
||||||
#[inline]
|
|
||||||
pub fn texture_options(mut self, texture_options: TextureOptions) -> Self {
|
|
||||||
self.texture_options = texture_options;
|
|
||||||
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 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 (rect, response) = ui.allocate_exact_size(self.size(), self.sense);
|
|
||||||
self.paint_at(ui, rect);
|
|
||||||
response
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct ImageOptions {
|
pub struct ImageOptions {
|
||||||
|
|
@ -669,7 +663,7 @@ impl Default for ImageOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paint a `SizedTexture` as an image according to some `ImageOptions` at a given `rect`.
|
/// 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) {
|
pub fn paint_image_at(ui: &Ui, rect: Rect, options: &ImageOptions, texture: &SizedTexture) {
|
||||||
if !ui.is_rect_visible(rect) {
|
if !ui.is_rect_visible(rect) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ pub mod text_edit;
|
||||||
pub use button::*;
|
pub use button::*;
|
||||||
pub use drag_value::DragValue;
|
pub use drag_value::DragValue;
|
||||||
pub use hyperlink::*;
|
pub use hyperlink::*;
|
||||||
pub use image::{Image, ImageFit, ImageOptions, ImageSize, ImageSource, RawImage};
|
pub use image::{paint_image_at, Image, ImageFit, ImageOptions, ImageSize, ImageSource};
|
||||||
pub use label::*;
|
pub use label::*;
|
||||||
pub use progress_bar::ProgressBar;
|
pub use progress_bar::ProgressBar;
|
||||||
pub use selected_label::SelectableLabel;
|
pub use selected_label::SelectableLabel;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use epaint::{emath::lerp, vec2, Color32, Pos2, Shape, Stroke};
|
use epaint::{emath::lerp, vec2, Color32, Pos2, Rect, Shape, Stroke};
|
||||||
|
|
||||||
use crate::{Response, Sense, Ui, Widget};
|
use crate::{Response, Sense, Ui, Widget};
|
||||||
|
|
||||||
|
|
@ -31,21 +31,14 @@ impl Spinner {
|
||||||
self.color = Some(color.into());
|
self.color = Some(color.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for Spinner {
|
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
|
||||||
let size = self
|
|
||||||
.size
|
|
||||||
.unwrap_or_else(|| ui.style().spacing.interact_size.y);
|
|
||||||
let color = self
|
|
||||||
.color
|
|
||||||
.unwrap_or_else(|| ui.visuals().strong_text_color());
|
|
||||||
let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover());
|
|
||||||
|
|
||||||
|
pub fn paint_at(&self, ui: &Ui, rect: Rect) {
|
||||||
if ui.is_rect_visible(rect) {
|
if ui.is_rect_visible(rect) {
|
||||||
ui.ctx().request_repaint();
|
ui.ctx().request_repaint();
|
||||||
|
|
||||||
|
let color = self
|
||||||
|
.color
|
||||||
|
.unwrap_or_else(|| ui.visuals().strong_text_color());
|
||||||
let radius = (rect.height() / 2.0) - 2.0;
|
let radius = (rect.height() / 2.0) - 2.0;
|
||||||
let n_points = 20;
|
let n_points = 20;
|
||||||
let time = ui.input(|i| i.time);
|
let time = ui.input(|i| i.time);
|
||||||
|
|
@ -61,6 +54,16 @@ impl Widget for Spinner {
|
||||||
ui.painter()
|
ui.painter()
|
||||||
.add(Shape::line(points, Stroke::new(3.0, color)));
|
.add(Shape::line(points, Stroke::new(3.0, color)));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Spinner {
|
||||||
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
|
let size = self
|
||||||
|
.size
|
||||||
|
.unwrap_or_else(|| ui.style().spacing.interact_size.y);
|
||||||
|
let (rect, response) = ui.allocate_exact_size(vec2(size, size), Sense::hover());
|
||||||
|
self.paint_at(ui, rect);
|
||||||
|
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ chrono = { version = "0.4", default-features = false, features = [
|
||||||
eframe = { version = "0.22.0", path = "../eframe", default-features = false }
|
eframe = { version = "0.22.0", path = "../eframe", default-features = false }
|
||||||
egui = { version = "0.22.0", path = "../egui", features = [
|
egui = { version = "0.22.0", path = "../egui", features = [
|
||||||
"extra_debug_asserts",
|
"extra_debug_asserts",
|
||||||
|
"log",
|
||||||
] }
|
] }
|
||||||
egui_demo_lib = { version = "0.22.0", path = "../egui_demo_lib", features = [
|
egui_demo_lib = { version = "0.22.0", path = "../egui_demo_lib", features = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
@ -45,8 +46,9 @@ log = { version = "0.4", features = ["std"] }
|
||||||
# Optional dependencies:
|
# Optional dependencies:
|
||||||
|
|
||||||
bytemuck = { version = "1.7.1", optional = true }
|
bytemuck = { version = "1.7.1", optional = true }
|
||||||
egui_extras = { version = "0.22.0", optional = true, path = "../egui_extras", features = [
|
egui_extras = { version = "0.22.0", path = "../egui_extras", features = [
|
||||||
"log",
|
"log",
|
||||||
|
"image",
|
||||||
] }
|
] }
|
||||||
rfd = { version = "0.11", optional = true }
|
rfd = { version = "0.11", optional = true }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
#![allow(deprecated)]
|
use egui::Image;
|
||||||
|
|
||||||
use egui_extras::RetainedImage;
|
|
||||||
use poll_promise::Promise;
|
use poll_promise::Promise;
|
||||||
|
|
||||||
struct Resource {
|
struct Resource {
|
||||||
|
|
@ -10,7 +8,7 @@ struct Resource {
|
||||||
text: Option<String>,
|
text: Option<String>,
|
||||||
|
|
||||||
/// If set, the response was an image.
|
/// If set, the response was an image.
|
||||||
image: Option<RetainedImage>,
|
image: Option<Image<'static>>,
|
||||||
|
|
||||||
/// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md").
|
/// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md").
|
||||||
colored_text: Option<ColoredText>,
|
colored_text: Option<ColoredText>,
|
||||||
|
|
@ -19,21 +17,27 @@ struct Resource {
|
||||||
impl Resource {
|
impl Resource {
|
||||||
fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self {
|
fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self {
|
||||||
let content_type = response.content_type().unwrap_or_default();
|
let content_type = response.content_type().unwrap_or_default();
|
||||||
let image = if content_type.starts_with("image/") {
|
if content_type.starts_with("image/") {
|
||||||
RetainedImage::from_image_bytes(&response.url, &response.bytes).ok()
|
ctx.include_bytes(response.url.clone(), response.bytes.clone());
|
||||||
|
let image = Image::from_uri(response.url.clone());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
response,
|
||||||
|
text: None,
|
||||||
|
colored_text: None,
|
||||||
|
image: Some(image),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
let text = response.text();
|
||||||
};
|
let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text));
|
||||||
|
let text = text.map(|text| text.to_owned());
|
||||||
|
|
||||||
let text = response.text();
|
Self {
|
||||||
let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text));
|
response,
|
||||||
let text = text.map(|text| text.to_owned());
|
text,
|
||||||
|
colored_text,
|
||||||
Self {
|
image: None,
|
||||||
response,
|
}
|
||||||
text,
|
|
||||||
image,
|
|
||||||
colored_text,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -65,6 +69,7 @@ impl eframe::App for HttpApp {
|
||||||
});
|
});
|
||||||
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
let prev_url = self.url.clone();
|
||||||
let trigger_fetch = ui_url(ui, frame, &mut self.url);
|
let trigger_fetch = ui_url(ui, frame, &mut self.url);
|
||||||
|
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
|
@ -79,6 +84,7 @@ impl eframe::App for HttpApp {
|
||||||
let (sender, promise) = Promise::new();
|
let (sender, promise) = Promise::new();
|
||||||
let request = ehttp::Request::get(&self.url);
|
let request = ehttp::Request::get(&self.url);
|
||||||
ehttp::fetch(request, move |response| {
|
ehttp::fetch(request, move |response| {
|
||||||
|
ctx.forget_image(&prev_url);
|
||||||
ctx.request_repaint(); // wake up UI thread
|
ctx.request_repaint(); // wake up UI thread
|
||||||
let resource = response.map(|response| Resource::from_response(&ctx, response));
|
let resource = response.map(|response| Resource::from_response(&ctx, response));
|
||||||
sender.send(resource);
|
sender.send(resource);
|
||||||
|
|
@ -195,9 +201,7 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(image) = image {
|
if let Some(image) = image {
|
||||||
let mut size = image.size_vec2();
|
ui.add(image.clone());
|
||||||
size *= (ui.available_width() / size.x).min(1.0);
|
|
||||||
image.show_size(ui, size);
|
|
||||||
} else if let Some(colored_text) = colored_text {
|
} else if let Some(colored_text) = colored_text {
|
||||||
colored_text.ui(ui);
|
colored_text.ui(ui);
|
||||||
} else if let Some(text) = &text {
|
} else if let Some(text) = &text {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ pub struct ImageViewer {
|
||||||
chosen_fit: ChosenFit,
|
chosen_fit: ChosenFit,
|
||||||
fit: ImageFit,
|
fit: ImageFit,
|
||||||
maintain_aspect_ratio: bool,
|
maintain_aspect_ratio: bool,
|
||||||
max_size: Option<Vec2>,
|
max_size: Vec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
|
@ -43,7 +43,7 @@ impl Default for ImageViewer {
|
||||||
chosen_fit: ChosenFit::Fraction,
|
chosen_fit: ChosenFit::Fraction,
|
||||||
fit: ImageFit::Fraction(Vec2::splat(1.0)),
|
fit: ImageFit::Fraction(Vec2::splat(1.0)),
|
||||||
maintain_aspect_ratio: true,
|
maintain_aspect_ratio: true,
|
||||||
max_size: None,
|
max_size: Vec2::splat(2048.0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -160,10 +160,10 @@ impl eframe::App for ImageViewer {
|
||||||
ui.add(Slider::new(&mut fract.y, 0.0..=1.0).text("height"));
|
ui.add(Slider::new(&mut fract.y, 0.0..=1.0).text("height"));
|
||||||
}
|
}
|
||||||
ChosenFit::OriginalSize => {
|
ChosenFit::OriginalSize => {
|
||||||
if !matches!(self.fit, ImageFit::Original(_)) {
|
if !matches!(self.fit, ImageFit::Original { .. }) {
|
||||||
self.fit = ImageFit::Original(Some(1.0));
|
self.fit = ImageFit::Original { scale: 1.0 };
|
||||||
}
|
}
|
||||||
let ImageFit::Original(Some(scale)) = &mut self.fit else {
|
let ImageFit::Original{scale} = &mut self.fit else {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
ui.add(Slider::new(scale, 0.1..=4.0).text("scale"));
|
ui.add(Slider::new(scale, 0.1..=4.0).text("scale"));
|
||||||
|
|
@ -173,21 +173,8 @@ impl eframe::App for ImageViewer {
|
||||||
// max size
|
// max size
|
||||||
ui.add_space(5.0);
|
ui.add_space(5.0);
|
||||||
ui.label("The calculated size will not exceed the maximum size");
|
ui.label("The calculated size will not exceed the maximum size");
|
||||||
let had_max_size = self.max_size.is_some();
|
ui.add(Slider::new(&mut self.max_size.x, 0.0..=2048.0).text("width"));
|
||||||
let mut has_max_size = had_max_size;
|
ui.add(Slider::new(&mut self.max_size.y, 0.0..=2048.0).text("height"));
|
||||||
ui.checkbox(&mut has_max_size, "Max size");
|
|
||||||
match (had_max_size, has_max_size) {
|
|
||||||
(true, false) => self.max_size = None,
|
|
||||||
(false, true) => {
|
|
||||||
self.max_size = Some(ui.available_size());
|
|
||||||
}
|
|
||||||
(true, true) | (false, false) => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(max_size) = self.max_size.as_mut() {
|
|
||||||
ui.add(Slider::new(&mut max_size.x, 0.0..=2048.0).text("width"));
|
|
||||||
ui.add(Slider::new(&mut max_size.y, 0.0..=2048.0).text("height"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// aspect ratio
|
// aspect ratio
|
||||||
ui.add_space(5.0);
|
ui.add_space(5.0);
|
||||||
|
|
@ -209,7 +196,7 @@ impl eframe::App for ImageViewer {
|
||||||
});
|
});
|
||||||
image = image.rotate(angle, origin);
|
image = image.rotate(angle, origin);
|
||||||
match self.fit {
|
match self.fit {
|
||||||
ImageFit::Original(scale) => image = image.fit_to_original_size(scale),
|
ImageFit::Original { scale } => image = image.fit_to_original_size(scale),
|
||||||
ImageFit::Fraction(fract) => image = image.fit_to_fraction(fract),
|
ImageFit::Fraction(fract) => image = image.fit_to_fraction(fract),
|
||||||
ImageFit::Exact(size) => image = image.fit_to_exact_size(size),
|
ImageFit::Exact(size) => image = image.fit_to_exact_size(size),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,6 @@ pub struct WrapApp {
|
||||||
|
|
||||||
impl WrapApp {
|
impl WrapApp {
|
||||||
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
#[cfg(feature = "image_viewer")]
|
|
||||||
egui_extras::loaders::install(&_cc.egui_ctx);
|
egui_extras::loaders::install(&_cc.egui_ctx);
|
||||||
|
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
|
|
@ -88,7 +88,7 @@ impl ColorTest {
|
||||||
let texel_offset = 0.5 / (g.0.len() as f32);
|
let texel_offset = 0.5 / (g.0.len() as f32);
|
||||||
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
|
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
|
||||||
ui.add(
|
ui.add(
|
||||||
RawImage::new((tex.id(), GRADIENT_SIZE))
|
Image::from_texture((tex.id(), GRADIENT_SIZE))
|
||||||
.tint(vertex_color)
|
.tint(vertex_color)
|
||||||
.uv(uv),
|
.uv(uv),
|
||||||
)
|
)
|
||||||
|
|
@ -230,7 +230,7 @@ impl ColorTest {
|
||||||
let texel_offset = 0.5 / (gradient.0.len() as f32);
|
let texel_offset = 0.5 / (gradient.0.len() as f32);
|
||||||
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
|
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
|
||||||
ui.add(
|
ui.add(
|
||||||
RawImage::new((tex.id(), GRADIENT_SIZE))
|
Image::from_texture((tex.id(), GRADIENT_SIZE))
|
||||||
.bg_fill(bg_fill)
|
.bg_fill(bg_fill)
|
||||||
.uv(uv),
|
.uv(uv),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,6 @@ pub struct WidgetGallery {
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
#[cfg_attr(feature = "serde", serde(skip))]
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
date: Option<chrono::NaiveDate>,
|
date: Option<chrono::NaiveDate>,
|
||||||
|
|
||||||
#[cfg_attr(feature = "serde", serde(skip))]
|
|
||||||
texture: Option<egui::TextureHandle>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for WidgetGallery {
|
impl Default for WidgetGallery {
|
||||||
|
|
@ -39,7 +36,6 @@ impl Default for WidgetGallery {
|
||||||
animate_progress_bar: false,
|
animate_progress_bar: false,
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
date: None,
|
date: None,
|
||||||
texture: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,14 +107,8 @@ impl WidgetGallery {
|
||||||
animate_progress_bar,
|
animate_progress_bar,
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
date,
|
date,
|
||||||
texture,
|
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let texture: &egui::TextureHandle = texture.get_or_insert_with(|| {
|
|
||||||
ui.ctx()
|
|
||||||
.load_texture("example", egui::ColorImage::example(), Default::default())
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add(doc_link_label("Label", "label,heading"));
|
ui.add(doc_link_label("Label", "label,heading"));
|
||||||
ui.label("Welcome to the widget gallery!");
|
ui.label("Welcome to the widget gallery!");
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
@ -206,15 +196,16 @@ impl WidgetGallery {
|
||||||
ui.color_edit_button_srgba(color);
|
ui.color_edit_button_srgba(color);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
let img_size = 16.0 * texture.size_vec2() / texture.size_vec2().y;
|
|
||||||
|
|
||||||
ui.add(doc_link_label("Image", "Image"));
|
ui.add(doc_link_label("Image", "Image"));
|
||||||
ui.raw_image((texture.id(), img_size));
|
let egui_icon = egui::include_image!("../../assets/icon.png");
|
||||||
|
ui.add(egui::Image::new(egui_icon.clone()));
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
ui.add(doc_link_label("ImageButton", "ImageButton"));
|
ui.add(doc_link_label("ImageButton", "ImageButton"));
|
||||||
if ui
|
if ui
|
||||||
.add(egui::ImageButton::new((texture.id(), img_size)))
|
.add(egui::ImageButton::new(
|
||||||
|
egui::Image::from(egui_icon).max_size(egui::Vec2::splat(16.0)),
|
||||||
|
))
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
*boolean = !*boolean;
|
*boolean = !*boolean;
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ impl RetainedImage {
|
||||||
// We need to convert the SVG to a texture to display it:
|
// We need to convert the SVG to a texture to display it:
|
||||||
// Future improvement: tell backend to do mip-mapping of the image to
|
// Future improvement: tell backend to do mip-mapping of the image to
|
||||||
// make it look smoother when downsized.
|
// make it look smoother when downsized.
|
||||||
ui.raw_image((self.texture_id(ui.ctx()), desired_size))
|
ui.image((self.texture_id(ui.ctx()), desired_size))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
mod datepicker;
|
mod datepicker;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
pub mod image;
|
pub mod image;
|
||||||
mod layout;
|
mod layout;
|
||||||
pub mod loaders;
|
pub mod loaders;
|
||||||
|
|
@ -23,6 +24,7 @@ mod table;
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
pub use crate::datepicker::DatePickerButton;
|
pub use crate::datepicker::DatePickerButton;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
pub use crate::image::RetainedImage;
|
pub use crate::image::RetainedImage;
|
||||||
pub(crate) use crate::layout::StripLayout;
|
pub(crate) use crate::layout::StripLayout;
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,7 @@ fn main() {
|
||||||
egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| {
|
egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| {
|
||||||
if ui
|
if ui
|
||||||
.add(egui::Button::image_and_text(
|
.add(egui::Button::image_and_text(
|
||||||
texture_id,
|
(texture_id, button_image_size),
|
||||||
button_image_size,
|
|
||||||
"Quit",
|
"Quit",
|
||||||
))
|
))
|
||||||
.clicked()
|
.clicked()
|
||||||
|
|
|
||||||
|
|
@ -1234,12 +1234,19 @@ impl PlotItem for PlotImage {
|
||||||
Rect::from_two_pos(left_top_screen, right_bottom_screen)
|
Rect::from_two_pos(left_top_screen, right_bottom_screen)
|
||||||
};
|
};
|
||||||
let screen_rotation = -*rotation as f32;
|
let screen_rotation = -*rotation as f32;
|
||||||
RawImage::new((*texture_id, image_screen_rect.size()))
|
|
||||||
.bg_fill(*bg_fill)
|
egui::paint_image_at(
|
||||||
.tint(*tint)
|
ui,
|
||||||
.uv(*uv)
|
image_screen_rect,
|
||||||
.rotate(screen_rotation, Vec2::splat(0.5))
|
&ImageOptions {
|
||||||
.paint_at(ui, image_screen_rect);
|
uv: *uv,
|
||||||
|
bg_fill: *bg_fill,
|
||||||
|
tint: *tint,
|
||||||
|
rotation: Some((Rot2::from_angle(screen_rotation), Vec2::splat(0.5))),
|
||||||
|
rounding: Rounding::ZERO,
|
||||||
|
},
|
||||||
|
&(*texture_id, image_screen_rect.size()).into(),
|
||||||
|
);
|
||||||
if *highlight {
|
if *highlight {
|
||||||
let center = image_screen_rect.center();
|
let center = image_screen_rect.center();
|
||||||
let rotation = Rot2::from_angle(screen_rotation);
|
let rotation = Rot2::from_angle(screen_rotation);
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ impl eframe::App for MyApp {
|
||||||
.fit_to_fraction(vec2(1.0, 0.5)),
|
.fit_to_fraction(vec2(1.0, 0.5)),
|
||||||
);
|
);
|
||||||
ui.add(
|
ui.add(
|
||||||
egui::Image::new("https://picsum.photos/seed/1.759706314/1024".into())
|
egui::Image::new("https://picsum.photos/seed/1.759706314/1024")
|
||||||
.rounding(egui::Rounding::same(10.0)),
|
.rounding(egui::Rounding::same(10.0)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ impl eframe::App for MyApp {
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(texture) = self.texture.as_ref() {
|
if let Some(texture) = self.texture.as_ref() {
|
||||||
ui.raw_image((texture.id(), ui.available_size()));
|
ui.image((texture.id(), ui.available_size()));
|
||||||
} else {
|
} else {
|
||||||
ui.spinner();
|
ui.spinner();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue