Final polish for new image loading (#3328)
* add egui logo to widget gallery * improve "no image loaders" error message * rework static URIs to accept `Cow<'static>` * remove `RetainedImage` from `http_app` in `egui_demo_app` * hide `RetainedImage` from docs * use `ui.image`/`Image` over `RawImage` * remove last remanant of `RawImage` * remove unused doc link * add style option to disable image spinners * use `Into<Image>` instead of `Into<ImageSource>` to allow configuring the underlying image * propagate `image_options` through `ImageButton` * calculate image size properly in `Button` * properly calculate size in `ImageButton` * Update crates/egui/src/widgets/image.rs Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com> * improve no image loaders error message * add `size()` helper to `TexturePoll` * try get size from poll in `Button` * add `paint_at` to `Spinner` * use `Spinner::paint_at` and hover on image button response * `show_spinner` -> `show_loading_spinner` * avoid `allocate_ui` in `Image` when painting spinner * make icon smaller + remove old texture * add `load_and_calculate_size` + expose `paint_image_at` * update `egui_plot` to paint image in the right place * Add helpers for painting an ImageSource directly * Use max_size=INF as default * Use new API in WidgetGallery * Make egui_demo_app work by default * Remove Option from scale * Refactor ImageSize * Fix docstring * Small refactor --------- Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
parent
fc3bddd0cf
commit
67a3fcae38
|
|
@ -605,9 +605,8 @@ impl Renderer {
|
|||
|
||||
/// Get the WGPU texture and bind group associated to a texture that has been allocated by egui.
|
||||
///
|
||||
/// 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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
use super::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DefaultBytesLoader {
|
||||
cache: Mutex<HashMap<Cow<'static, str>, Bytes>>,
|
||||
}
|
||||
|
||||
impl DefaultBytesLoader {
|
||||
pub fn insert(&self, uri: impl Into<Cow<'static, str>>, bytes: impl Into<Bytes>) {
|
||||
self.cache
|
||||
.lock()
|
||||
.entry(uri.into())
|
||||
.or_insert_with_key(|uri| {
|
||||
let bytes: Bytes = bytes.into();
|
||||
|
||||
#[cfg(feature = "log")]
|
||||
log::trace!("loaded {} bytes for uri {uri:?}", bytes.len());
|
||||
|
||||
bytes
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl BytesLoader for DefaultBytesLoader {
|
||||
fn id(&self) -> &str {
|
||||
generate_loader_id!(DefaultBytesLoader)
|
||||
}
|
||||
|
||||
fn load(&self, _: &Context, uri: &str) -> BytesLoadResult {
|
||||
match self.cache.lock().get(uri).cloned() {
|
||||
Some(bytes) => Ok(BytesPoll::Ready {
|
||||
size: None,
|
||||
bytes,
|
||||
mime: None,
|
||||
}),
|
||||
None => Err(LoadError::NotSupported),
|
||||
}
|
||||
}
|
||||
|
||||
fn forget(&self, uri: &str) {
|
||||
#[cfg(feature = "log")]
|
||||
log::trace!("forget {uri:?}");
|
||||
|
||||
let _ = self.cache.lock().remove(uri);
|
||||
}
|
||||
|
||||
fn forget_all(&self) {
|
||||
#[cfg(feature = "log")]
|
||||
log::trace!("forget all");
|
||||
|
||||
self.cache.lock().clear();
|
||||
}
|
||||
|
||||
fn byte_size(&self) -> usize {
|
||||
self.cache.lock().values().map(|bytes| bytes.len()).sum()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
use super::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DefaultTextureLoader {
|
||||
cache: Mutex<HashMap<(String, TextureOptions), TextureHandle>>,
|
||||
}
|
||||
|
||||
impl TextureLoader for DefaultTextureLoader {
|
||||
fn id(&self) -> &str {
|
||||
crate::generate_loader_id!(DefaultTextureLoader)
|
||||
}
|
||||
|
||||
fn load(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
uri: &str,
|
||||
texture_options: TextureOptions,
|
||||
size_hint: SizeHint,
|
||||
) -> TextureLoadResult {
|
||||
let mut cache = self.cache.lock();
|
||||
if let Some(handle) = cache.get(&(uri.into(), texture_options)) {
|
||||
let texture = SizedTexture::from_handle(handle);
|
||||
Ok(TexturePoll::Ready { texture })
|
||||
} else {
|
||||
match ctx.try_load_image(uri, size_hint)? {
|
||||
ImagePoll::Pending { size } => Ok(TexturePoll::Pending { size }),
|
||||
ImagePoll::Ready { image } => {
|
||||
let handle = ctx.load_texture(uri, image, texture_options);
|
||||
let texture = SizedTexture::from_handle(&handle);
|
||||
cache.insert((uri.into(), texture_options), handle);
|
||||
Ok(TexturePoll::Ready { texture })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn forget(&self, uri: &str) {
|
||||
#[cfg(feature = "log")]
|
||||
log::trace!("forget {uri:?}");
|
||||
|
||||
self.cache.lock().retain(|(u, _), _| u != uri);
|
||||
}
|
||||
|
||||
fn forget_all(&self) {
|
||||
#[cfg(feature = "log")]
|
||||
log::trace!("forget all");
|
||||
|
||||
self.cache.lock().clear();
|
||||
}
|
||||
|
||||
fn end_frame(&self, _: usize) {}
|
||||
|
||||
fn byte_size(&self) -> usize {
|
||||
self.cache
|
||||
.lock()
|
||||
.values()
|
||||
.map(|texture| texture.byte_size())
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
|
|
@ -207,6 +207,9 @@ pub struct Style {
|
|||
///
|
||||
/// This only affects a few egui widgets.
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue