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:
Jan Procházka 2023-09-13 16:27:08 +02:00 committed by GitHub
parent fc3bddd0cf
commit 67a3fcae38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 536 additions and 503 deletions

View File

@ -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,

View File

@ -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,

View File

@ -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)),
)
}; };
} }

View File

@ -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>;

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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));
} }
} }

View File

@ -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)
} }
} }
} }

View File

@ -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())),

View File

@ -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;
} }

View File

@ -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;

View File

@ -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
} }

View File

@ -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 }

View File

@ -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 {

View File

@ -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),
} }

View File

@ -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

View File

@ -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),
) )

View File

@ -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;

View File

@ -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))
} }
} }

View File

@ -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;

View File

@ -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()

View File

@ -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);

View File

@ -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)),
); );
}); });

View File

@ -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();
} }