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.
///
/// This could be used by custom paint hooks to render images that have been added through with
/// [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html)
/// or [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture).
/// This could be used by custom paint hooks to render images that have been added through
/// [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture).
pub fn texture(
&self,
id: &epaint::TextureId,

View File

@ -1,5 +1,6 @@
#![warn(missing_docs)] // Let's keep `Context` well-documented.
use std::borrow::Cow;
use std::sync::Arc;
use crate::load::Bytes;
@ -1145,7 +1146,7 @@ impl Context {
/// });
///
/// // 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);
size *= (max_preview_size.x / size.x).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| {
// show larger on hover
let max_size = 0.5 * ui.ctx().screen_rect().size();
let mut size = vec2(w as f32, h as f32);
size *= max_size.x / size.x.max(max_size.x);
size *= max_size.y / size.y.max(max_size.y);
ui.raw_image(SizedTexture::new(texture_id, size));
ui.image(SizedTexture::new(texture_id, size));
});
ui.label(format!("{w} x {h}"));
@ -1911,8 +1912,8 @@ impl Context {
/// Associate some static bytes with a `uri`.
///
/// The same `uri` may be passed to [`Ui::image`] later to load the bytes as an image.
pub fn include_bytes(&self, uri: &'static str, bytes: impl Into<Bytes>) {
self.loaders().include.insert(uri, bytes.into());
pub fn include_bytes(&self, uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) {
self.loaders().include.insert(uri, bytes);
}
/// Returns `true` if the chain of bytes, image, or texture loaders
@ -2038,17 +2039,25 @@ impl Context {
///
/// # Errors
/// 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::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`!
///
/// [no_image_loaders]: crate::load::LoadError::NoImageLoaders
/// [not_supported]: crate::load::LoadError::NotSupported
/// [custom]: crate::load::LoadError::Custom
pub fn try_load_image(&self, uri: &str, size_hint: load::SizeHint) -> load::ImageLoadResult {
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) {
Err(load::LoadError::NotSupported) => continue,
result => return result,

View File

@ -84,7 +84,7 @@
//! ui.separator();
//!
//! # 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.label("Not much, as it turns out");
@ -442,7 +442,10 @@ pub fn warn_if_debug_build(ui: &mut crate::Ui) {
#[macro_export]
macro_rules! include_image {
($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
//! 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 ahash::HashMap;
use epaint::mutex::Mutex;
@ -59,6 +64,7 @@ use epaint::util::FloatOrd;
use epaint::util::OrderedFloat;
use epaint::TextureHandle;
use epaint::{textures::TextureOptions, ColorImage, TextureId, Vec2};
use std::borrow::Cow;
use std::fmt::Debug;
use std::ops::Deref;
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.
#[derive(Clone, Debug)]
pub enum LoadError {
/// There are no image loaders installed.
NoImageLoaders,
/// This loader does not support this protocol or image format.
NotSupported,
@ -76,6 +85,9 @@ pub enum LoadError {
impl Display for LoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
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::Custom(message) => f.write_str(message),
}
@ -342,7 +354,7 @@ pub trait ImageLoader {
}
/// A texture with a known size.
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SizedTexture {
pub id: TextureId,
pub size: Vec2,
@ -370,7 +382,13 @@ impl SizedTexture {
impl From<(TextureId, Vec2)> for SizedTexture {
#[inline]
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
/// contains an optional `size`, which may be used during layout to
/// pre-allocate space the image.
#[derive(Clone)]
#[derive(Clone, Copy)]
pub enum TexturePoll {
/// Texture is loading.
Pending {
@ -391,6 +409,15 @@ pub enum TexturePoll {
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>;
/// Represents a loader capable of loading a full texture.
@ -447,99 +474,6 @@ pub trait TextureLoader {
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 ImageLoaderImpl = Arc<dyn ImageLoader + 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.
pub explanation_tooltips: bool,
/// Show a spinner when loading an image.
pub image_loading_spinners: bool,
}
impl Style {
@ -738,6 +741,7 @@ impl Default for Style {
animation_time: 1.0 / 12.0,
debug: Default::default(),
explanation_tooltips: false,
image_loading_spinners: true,
}
}
}
@ -990,6 +994,7 @@ impl Style {
animation_time,
debug,
explanation_tooltips,
image_loading_spinners,
} = self;
visuals.light_dark_radio_buttons(ui);
@ -1057,6 +1062,9 @@ impl Style {
"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));
}
}

View File

@ -5,7 +5,6 @@ use std::sync::Arc;
use epaint::mutex::RwLock;
use crate::load::SizedTexture;
use crate::{
containers::*, ecolor::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer,
util::IdTypeMap, widgets::*, *,
@ -1582,47 +1581,10 @@ impl Ui {
/// from a file with a statically known path, unless you really want to
/// load it at runtime instead!
///
/// See also [`crate::Image`], [`crate::ImageSource`] and [`Self::raw_image`].
/// See also [`crate::Image`], [`crate::ImageSource`].
#[inline]
pub fn image<'a>(&mut self, source: impl Into<ImageSource<'a>>) -> Response {
Image::new(source.into()).ui(self)
}
/// Show an image created from a sized texture.
///
/// You may use this method over [`Ui::image`] if you already have a [`TextureHandle`]
/// or a [`SizedTexture`].
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// struct MyImage {
/// texture: Option<egui::TextureHandle>,
/// }
///
/// impl MyImage {
/// fn ui(&mut self, ui: &mut egui::Ui) {
/// let texture = self
/// .texture
/// .get_or_insert_with(|| {
/// // Load the texture only once.
/// ui.ctx().load_texture(
/// "my-image",
/// egui::ColorImage::example(),
/// Default::default()
/// )
/// });
///
/// // Show the image:
/// ui.raw_image((texture.id(), texture.size_vec2()));
/// }
/// }
/// # });
/// ```
///
/// See also [`crate::RawImage`].
#[inline]
pub fn raw_image(&mut self, texture: impl Into<SizedTexture>) -> Response {
RawImage::new(texture).ui(self)
Image::new(source).ui(self)
}
}
@ -2236,13 +2198,13 @@ impl Ui {
#[inline]
pub fn menu_image_button<'a, R>(
&mut self,
image_source: impl Into<ImageSource<'a>>,
image: impl Into<Image<'a>>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
if let Some(menu_state) = self.menu_state.clone() {
menu::submenu_button(self, menu_state, String::new(), add_contents)
} else {
menu::menu_image_button(self, ImageButton::new(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);`"]
pub struct Button {
pub struct Button<'a> {
text: WidgetText,
shortcut_text: WidgetText,
wrap: Option<bool>,
@ -34,10 +34,10 @@ pub struct Button {
frame: Option<bool>,
min_size: Vec2,
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 {
Self {
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.
#[allow(clippy::needless_pass_by_value)]
pub fn image_and_text(
texture_id: TextureId,
image_size: impl Into<Vec2>,
text: impl Into<WidgetText>,
) -> Self {
pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
Self {
image: Some(widgets::RawImage::new(SizedTexture {
id: texture_id,
size: image_size.into(),
})),
image: Some(image.into()),
..Self::new(text)
}
}
@ -142,7 +135,7 @@ impl Button {
}
}
impl Widget for Button {
impl Widget for Button<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let Button {
text,
@ -158,6 +151,11 @@ impl Widget for Button {
image,
} = 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 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;
if let Some(image) = &image {
text_wrap_width -= image.size().x + ui.spacing().icon_spacing;
if image.is_some() {
text_wrap_width -= image_size.x + ui.spacing().icon_spacing;
}
if !shortcut_text.is_empty() {
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));
let mut desired_size = text.size();
if let Some(image) = &image {
desired_size.x += image.size().x + ui.spacing().icon_spacing;
desired_size.y = desired_size.y.max(image.size().y);
if image.is_some() {
desired_size.x += image_size.x + ui.spacing().icon_spacing;
desired_size.y = desired_size.y.max(image_size.y);
}
if let Some(shortcut_text) = &shortcut_text {
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);
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()));
if ui.is_rect_visible(rect) {
@ -206,10 +204,10 @@ impl Widget for Button {
.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;
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,
)
} else {
@ -235,11 +233,23 @@ impl Widget for Button {
let image_rect = Rect::from_min_size(
pos2(
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> {
pub fn new(source: impl Into<ImageSource<'a>>) -> Self {
pub fn new(image: impl Into<Image<'a>>) -> Self {
Self {
image: Image::new(source.into()),
image: image.into(),
sense: Sense::click(),
frame: true,
selected: false,
@ -564,9 +574,9 @@ impl<'a> ImageButton<'a> {
.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_options = ImageOptions {
rounding,
..Default::default()
}; // apply rounding to the image
rounding, // apply rounding to the image
..self.image.image_options().clone()
};
crate::widgets::image::paint_image_at(ui, image_rect, &image_options, texture);
// Draw frame outline:
@ -581,7 +591,10 @@ impl<'a> ImageButton<'a> {
impl<'a> Widget for ImageButton<'a> {
fn ui(self, ui: &mut Ui) -> Response {
match self.image.load(ui) {
Ok(TexturePoll::Ready { texture }) => self.show(ui, &texture),
Ok(TexturePoll::Ready { mut texture }) => {
texture.size = self.image.calculate_size(ui.available_size(), texture.size);
self.show(ui, &texture)
}
Ok(TexturePoll::Pending { .. }) => ui
.spinner()
.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::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.
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
#[derive(Debug, Clone)]
@ -28,18 +26,36 @@ pub struct Image<'a> {
image_options: ImageOptions,
sense: Sense,
size: ImageSize,
pub(crate) show_loading_spinner: Option<bool>,
}
impl<'a> Image<'a> {
/// Load the image from some source.
pub fn new(source: ImageSource<'a>) -> Self {
Self {
source,
texture_options: Default::default(),
image_options: Default::default(),
sense: Sense::hover(),
size: Default::default(),
pub fn new(source: impl Into<ImageSource<'a>>) -> Self {
fn new_mono(source: ImageSource<'_>) -> Image<'_> {
let size = if let ImageSource::Texture(tex) = &source {
// User is probably expecting their texture to have
// the exact size of the provided `SizedTexture`.
ImageSize {
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.
@ -52,15 +68,15 @@ impl<'a> Image<'a> {
/// Load the image from an existing texture.
///
/// See [`ImageSource::Texture`].
pub fn from_texture(texture: SizedTexture) -> Self {
Self::new(ImageSource::Texture(texture))
pub fn from_texture(texture: impl Into<SizedTexture>) -> Self {
Self::new(ImageSource::Texture(texture.into()))
}
/// Load the image from some raw bytes.
///
/// See [`ImageSource::Bytes`].
pub fn from_bytes(uri: &'static str, bytes: impl Into<Bytes>) -> Self {
Self::new(ImageSource::Bytes(uri, bytes.into()))
pub fn from_bytes(uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) -> Self {
Self::new(ImageSource::Bytes(uri.into(), bytes.into()))
}
/// 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.
#[inline]
pub fn max_width(mut self, width: f32) -> Self {
match self.size.max_size.as_mut() {
Some(max_size) => max_size.x = width,
None => self.size.max_size = Some(Vec2::new(width, f32::INFINITY)),
}
self.size.max_size.x = width;
self
}
@ -87,10 +100,7 @@ impl<'a> Image<'a> {
/// No matter what the image is scaled to, it will never exceed this limit.
#[inline]
pub fn max_height(mut self, height: f32) -> Self {
match self.size.max_size.as_mut() {
Some(max_size) => max_size.y = height,
None => self.size.max_size = Some(Vec2::new(f32::INFINITY, height)),
}
self.size.max_size.y = height;
self
}
@ -98,7 +108,7 @@ impl<'a> Image<'a> {
///
/// No matter what the image is scaled to, it will never exceed this limit.
#[inline]
pub fn max_size(mut self, size: Option<Vec2>) -> Self {
pub fn max_size(mut self, size: Vec2) -> Self {
self.size.max_size = size;
self
}
@ -110,14 +120,14 @@ impl<'a> Image<'a> {
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.
///
/// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit.
#[inline]
pub fn fit_to_original_size(mut self, scale: Option<f32>) -> Self {
self.size.fit = ImageFit::Original(scale);
pub fn fit_to_original_size(mut self, scale: f32) -> Self {
self.size.fit = ImageFit::Original { scale };
self
}
@ -157,18 +167,21 @@ impl<'a> Image<'a> {
}
/// 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 {
self.image_options.uv = uv.into();
self
}
/// 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 {
self.image_options.bg_fill = bg_fill.into();
self
}
/// Multiply image color with this. Default is WHITE (no tint).
#[inline]
pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
self.image_options.tint = tint.into();
self
@ -183,6 +196,7 @@ impl<'a> Image<'a> {
///
/// Due to limitations in the current implementation,
/// this will turn off rounding of the image.
#[inline]
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
@ -195,6 +209,7 @@ impl<'a> Image<'a> {
///
/// Due to limitations in the current implementation,
/// this will turn off any rotation of the image.
#[inline]
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
self.image_options.rounding = rounding.into();
if self.image_options.rounding != Rounding::ZERO {
@ -202,14 +217,36 @@ impl<'a> Image<'a> {
}
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> {
/// Returns the size the image will occupy in the final UI.
#[inline]
pub fn calculate_size(&self, available_size: Vec2, image_size: Vec2) -> Vec2 {
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> {
match &self.source {
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> {
&self.source
}
@ -224,13 +267,9 @@ impl<'a> Image<'a> {
/// Get the `uri` that this image was constructed from.
///
/// This will return `<unknown>` for [`ImageSource::Texture`].
#[inline]
pub fn uri(&self) -> &str {
match &self.source {
ImageSource::Bytes(uri, _) => uri,
ImageSource::Uri(uri) => uri,
// Note: texture source is never in "loading" state
ImageSource::Texture(_) => "<unknown>",
}
self.source.uri().unwrap_or("<unknown>")
}
/// 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.
pub fn load(&self, ui: &Ui) -> TextureLoadResult {
match self.source.clone() {
ImageSource::Texture(texture) => Ok(TexturePoll::Ready { texture }),
ImageSource::Uri(uri) => ui.ctx().try_load_texture(
uri.as_ref(),
self.texture_options,
self.size.hint(ui.available_size()),
),
ImageSource::Bytes(uri, bytes) => {
ui.ctx().include_bytes(uri.as_ref(), bytes);
ui.ctx().try_load_texture(
uri.as_ref(),
self.texture_options,
self.size.hint(ui.available_size()),
)
}
}
let size_hint = self.size.hint(ui.available_size());
self.source
.clone()
.load(ui.ctx(), self.texture_options, size_hint)
}
#[inline]
pub fn paint_at(&self, ui: &mut Ui, rect: Rect, texture: &SizedTexture) {
paint_image_at(ui, rect, &self.image_options, texture);
}
@ -265,27 +293,28 @@ impl<'a> Image<'a> {
impl<'a> Widget for Image<'a> {
fn ui(self, ui: &mut Ui) -> Response {
match self.load(ui) {
Ok(TexturePoll::Ready { texture }) => {
let size = self.calculate_size(ui.available_size(), texture.size);
let (rect, response) = ui.allocate_exact_size(size, self.sense);
self.paint_at(ui, rect, &texture);
response
}
Ok(TexturePoll::Pending { size }) => match size {
Some(size) => {
let size = self.calculate_size(ui.available_size(), size);
ui.allocate_ui(size, |ui| {
ui.with_layout(Layout::centered_and_justified(Direction::TopDown), |ui| {
ui.spinner()
.on_hover_text(format!("Loading {:?}", self.uri()))
})
})
.response
Ok(texture_poll) => {
let texture_size = texture_poll.size();
let texture_size =
texture_size.unwrap_or_else(|| Vec2::splat(ui.style().spacing.interact_size.y));
let ui_size = self.calculate_size(ui.available_size(), texture_size);
let (rect, response) = ui.allocate_exact_size(ui_size, self.sense);
match texture_poll {
TexturePoll::Ready { texture } => {
self.paint_at(ui, rect, &texture);
response
}
TexturePoll::Pending { .. } => {
let show_spinner = self
.show_loading_spinner
.unwrap_or(ui.style().image_loading_spinners);
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
.colored_label(ui.visuals().error_fg_color, "")
.on_hover_text(err.to_string()),
@ -306,8 +335,8 @@ pub struct ImageSize {
/// Determines the maximum size of the image.
///
/// Defaults to `None`
pub max_size: Option<Vec2>,
/// Defaults to `Vec2::INFINITY` (no limit).
pub max_size: Vec2,
/// 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)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ImageFit {
/// Fit the image to its original size, optionally scaling it by some factor.
Original(Option<f32>),
/// Fit the image to its original size, scaled by some factor.
Original { scale: f32 },
/// Fit the image to a fraction of the available size.
Fraction(Vec2),
@ -333,6 +362,16 @@ pub enum ImageFit {
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 {
fn hint(&self, available_size: Vec2) -> SizeHint {
if self.maintain_aspect_ratio {
@ -340,15 +379,12 @@ impl ImageSize {
};
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::Exact(size) => size,
};
let fit = match self.max_size {
Some(extent) => fit.min(extent),
None => fit,
};
let fit = fit.min(self.max_size);
// `inf` on an axis means "any value"
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 {
match self.fit {
ImageFit::Original(scale) => {
let image_size = image_size * scale.unwrap_or(1.0);
if let Some(available_size) = self.max_size {
if image_size.x < available_size.x && image_size.y < available_size.y {
return image_size;
}
if self.maintain_aspect_ratio {
let ratio_x = available_size.x / image_size.x;
let ratio_y = available_size.y / image_size.y;
let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y };
let ratio = if ratio.is_infinite() { 1.0 } else { ratio };
return Vec2::new(image_size.x * ratio, image_size.y * ratio);
} else {
return image_size.min(available_size);
}
let Self {
maintain_aspect_ratio,
max_size,
fit,
} = *self;
match fit {
ImageFit::Original { scale } => {
let image_size = image_size * scale;
if image_size.x <= max_size.x && image_size.y <= max_size.y {
image_size
} else {
scale_to_fit(image_size, max_size, maintain_aspect_ratio)
}
image_size
}
ImageFit::Fraction(fract) => {
let available_size = available_size * fract;
let available_size = match self.max_size {
Some(max_size) => available_size.min(max_size),
None => available_size,
};
if self.maintain_aspect_ratio {
let ratio_x = available_size.x / image_size.x;
let ratio_y = available_size.y / image_size.y;
let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y };
let ratio = if ratio.is_infinite() { 1.0 } else { ratio };
return Vec2::new(image_size.x * ratio, image_size.y * ratio);
}
available_size
let scale_to_size = (available_size * fract).min(max_size);
scale_to_fit(image_size, scale_to_size, maintain_aspect_ratio)
}
ImageFit::Exact(size) => {
let available_size = size;
let available_size = match self.max_size {
Some(max_size) => available_size.min(max_size),
None => available_size,
};
if self.maintain_aspect_ratio {
let ratio_x = available_size.x / image_size.x;
let ratio_y = available_size.y / image_size.y;
let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y };
let ratio = if ratio.is_infinite() { 1.0 } else { ratio };
return Vec2::new(image_size.x * ratio, image_size.y * ratio);
}
available_size
let scale_to_size = size.min(max_size);
scale_to_fit(image_size, scale_to_size, maintain_aspect_ratio)
}
}
}
}
// 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 {
#[inline]
fn default() -> Self {
Self {
max_size: None,
max_size: Vec2::INFINITY,
fit: ImageFit::Fraction(Vec2::new(1.0, 1.0)),
maintain_aspect_ratio: true,
}
@ -452,8 +464,6 @@ pub enum ImageSource<'a> {
///
/// The user is responsible for loading the texture, determining its size,
/// and allocating a [`TextureId`] for it.
///
/// Note that a simpler API for this exists in [`RawImage`].
Texture(SizedTexture),
/// Load the image from some raw bytes.
@ -467,7 +477,84 @@ pub enum ImageSource<'a> {
/// See also [`include_image`] for an easy way to load and display static images.
///
/// 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> {
@ -507,123 +594,30 @@ impl<'a> From<Cow<'a, str>> for ImageSource<'a> {
impl<T: Into<Bytes>> From<(&'static str, T)> for ImageSource<'static> {
#[inline]
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())
}
}
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> {
fn from(value: T) -> Self {
Self::Texture(value.into())
}
}
/// A widget which displays a sized texture.
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
#[derive(Debug, Clone)]
pub struct RawImage {
texture: SizedTexture,
texture_options: TextureOptions,
image_options: ImageOptions,
sense: Sense,
}
impl RawImage {
/// Load the image from some source.
pub fn new(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)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
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`.
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) {
return;
}

View File

@ -22,7 +22,7 @@ pub mod text_edit;
pub use button::*;
pub use drag_value::DragValue;
pub use hyperlink::*;
pub use image::{Image, ImageFit, ImageOptions, ImageSize, ImageSource, RawImage};
pub use image::{paint_image_at, Image, ImageFit, ImageOptions, ImageSize, ImageSource};
pub use label::*;
pub use progress_bar::ProgressBar;
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};
@ -31,21 +31,14 @@ impl Spinner {
self.color = Some(color.into());
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) {
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 n_points = 20;
let time = ui.input(|i| i.time);
@ -61,6 +54,16 @@ impl Widget for Spinner {
ui.painter()
.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
}

View File

@ -36,6 +36,7 @@ chrono = { version = "0.4", default-features = false, features = [
eframe = { version = "0.22.0", path = "../eframe", default-features = false }
egui = { version = "0.22.0", path = "../egui", features = [
"extra_debug_asserts",
"log",
] }
egui_demo_lib = { version = "0.22.0", path = "../egui_demo_lib", features = [
"chrono",
@ -45,8 +46,9 @@ log = { version = "0.4", features = ["std"] }
# Optional dependencies:
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",
"image",
] }
rfd = { version = "0.11", optional = true }

View File

@ -1,6 +1,4 @@
#![allow(deprecated)]
use egui_extras::RetainedImage;
use egui::Image;
use poll_promise::Promise;
struct Resource {
@ -10,7 +8,7 @@ struct Resource {
text: Option<String>,
/// 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").
colored_text: Option<ColoredText>,
@ -19,21 +17,27 @@ struct Resource {
impl Resource {
fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self {
let content_type = response.content_type().unwrap_or_default();
let image = if content_type.starts_with("image/") {
RetainedImage::from_image_bytes(&response.url, &response.bytes).ok()
if content_type.starts_with("image/") {
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 {
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();
let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text));
let text = text.map(|text| text.to_owned());
Self {
response,
text,
image,
colored_text,
Self {
response,
text,
colored_text,
image: None,
}
}
}
}
@ -65,6 +69,7 @@ impl eframe::App for HttpApp {
});
egui::CentralPanel::default().show(ctx, |ui| {
let prev_url = self.url.clone();
let trigger_fetch = ui_url(ui, frame, &mut self.url);
ui.horizontal_wrapped(|ui| {
@ -79,6 +84,7 @@ impl eframe::App for HttpApp {
let (sender, promise) = Promise::new();
let request = ehttp::Request::get(&self.url);
ehttp::fetch(request, move |response| {
ctx.forget_image(&prev_url);
ctx.request_repaint(); // wake up UI thread
let resource = response.map(|response| Resource::from_response(&ctx, response));
sender.send(resource);
@ -195,9 +201,7 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
}
if let Some(image) = image {
let mut size = image.size_vec2();
size *= (ui.available_width() / size.x).min(1.0);
image.show_size(ui, size);
ui.add(image.clone());
} else if let Some(colored_text) = colored_text {
colored_text.ui(ui);
} else if let Some(text) = &text {

View File

@ -13,7 +13,7 @@ pub struct ImageViewer {
chosen_fit: ChosenFit,
fit: ImageFit,
maintain_aspect_ratio: bool,
max_size: Option<Vec2>,
max_size: Vec2,
}
#[derive(Clone, Copy, PartialEq, Eq)]
@ -43,7 +43,7 @@ impl Default for ImageViewer {
chosen_fit: ChosenFit::Fraction,
fit: ImageFit::Fraction(Vec2::splat(1.0)),
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"));
}
ChosenFit::OriginalSize => {
if !matches!(self.fit, ImageFit::Original(_)) {
self.fit = ImageFit::Original(Some(1.0));
if !matches!(self.fit, ImageFit::Original { .. }) {
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!()
};
ui.add(Slider::new(scale, 0.1..=4.0).text("scale"));
@ -173,21 +173,8 @@ impl eframe::App for ImageViewer {
// max size
ui.add_space(5.0);
ui.label("The calculated size will not exceed the maximum size");
let had_max_size = self.max_size.is_some();
let mut has_max_size = had_max_size;
ui.checkbox(&mut has_max_size, "Max size");
match (had_max_size, has_max_size) {
(true, false) => self.max_size = None,
(false, true) => {
self.max_size = Some(ui.available_size());
}
(true, true) | (false, false) => {}
}
if let Some(max_size) = self.max_size.as_mut() {
ui.add(Slider::new(&mut max_size.x, 0.0..=2048.0).text("width"));
ui.add(Slider::new(&mut max_size.y, 0.0..=2048.0).text("height"));
}
ui.add(Slider::new(&mut self.max_size.x, 0.0..=2048.0).text("width"));
ui.add(Slider::new(&mut self.max_size.y, 0.0..=2048.0).text("height"));
// aspect ratio
ui.add_space(5.0);
@ -209,7 +196,7 @@ impl eframe::App for ImageViewer {
});
image = image.rotate(angle, origin);
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::Exact(size) => image = image.fit_to_exact_size(size),
}

View File

@ -165,7 +165,6 @@ pub struct WrapApp {
impl WrapApp {
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
#[cfg(feature = "image_viewer")]
egui_extras::loaders::install(&_cc.egui_ctx);
#[allow(unused_mut)]

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 uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
ui.add(
RawImage::new((tex.id(), GRADIENT_SIZE))
Image::from_texture((tex.id(), GRADIENT_SIZE))
.tint(vertex_color)
.uv(uv),
)
@ -230,7 +230,7 @@ impl ColorTest {
let texel_offset = 0.5 / (gradient.0.len() as f32);
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
ui.add(
RawImage::new((tex.id(), GRADIENT_SIZE))
Image::from_texture((tex.id(), GRADIENT_SIZE))
.bg_fill(bg_fill)
.uv(uv),
)

View File

@ -21,9 +21,6 @@ pub struct WidgetGallery {
#[cfg(feature = "chrono")]
#[cfg_attr(feature = "serde", serde(skip))]
date: Option<chrono::NaiveDate>,
#[cfg_attr(feature = "serde", serde(skip))]
texture: Option<egui::TextureHandle>,
}
impl Default for WidgetGallery {
@ -39,7 +36,6 @@ impl Default for WidgetGallery {
animate_progress_bar: false,
#[cfg(feature = "chrono")]
date: None,
texture: None,
}
}
}
@ -111,14 +107,8 @@ impl WidgetGallery {
animate_progress_bar,
#[cfg(feature = "chrono")]
date,
texture,
} = 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.label("Welcome to the widget gallery!");
ui.end_row();
@ -206,15 +196,16 @@ impl WidgetGallery {
ui.color_edit_button_srgba(color);
ui.end_row();
let img_size = 16.0 * texture.size_vec2() / texture.size_vec2().y;
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.add(doc_link_label("ImageButton", "ImageButton"));
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()
{
*boolean = !*boolean;

View File

@ -191,7 +191,7 @@ impl RetainedImage {
// We need to convert the SVG to a texture to display it:
// Future improvement: tell backend to do mip-mapping of the image to
// make it look smoother when downsized.
ui.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")]
mod datepicker;
#[doc(hidden)]
pub mod image;
mod layout;
pub mod loaders;
@ -23,6 +24,7 @@ mod table;
#[cfg(feature = "chrono")]
pub use crate::datepicker::DatePickerButton;
#[doc(hidden)]
#[allow(deprecated)]
pub use crate::image::RetainedImage;
pub(crate) use crate::layout::StripLayout;

View File

@ -30,8 +30,7 @@ fn main() {
egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| {
if ui
.add(egui::Button::image_and_text(
texture_id,
button_image_size,
(texture_id, button_image_size),
"Quit",
))
.clicked()

View File

@ -1234,12 +1234,19 @@ impl PlotItem for PlotImage {
Rect::from_two_pos(left_top_screen, right_bottom_screen)
};
let screen_rotation = -*rotation as f32;
RawImage::new((*texture_id, image_screen_rect.size()))
.bg_fill(*bg_fill)
.tint(*tint)
.uv(*uv)
.rotate(screen_rotation, Vec2::splat(0.5))
.paint_at(ui, image_screen_rect);
egui::paint_image_at(
ui,
image_screen_rect,
&ImageOptions {
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 {
let center = image_screen_rect.center();
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)),
);
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)),
);
});

View File

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