Improved texture loading (#3315)
* rework loading around `Arc<Loaders>` * use `Bytes` instead of splitting api * remove unwraps in `texture_handle` * make `FileLoader` optional under `file` feature * hide http load error stack trace from UI * implement image fit * support more image sources * center spinner if we know size ahead of time * allocate final size for spinner * improve image format guessing * remove `ui.image`, `Image`, add `RawImage` * deprecate `RetainedImage` * `image2` -> `image` * add viewer example * update `examples/image` + remove `svg` and `download_image` exapmles * fix lints and tests * fix doc link * add image controls to `images` example * add more `From` str-like types * add api to forget all images * fix max size * do not scale original size unless necessary * fix doc link * add more docs for `Image` and `RawImage` * make paint_at `pub` * update `ImageButton` to use new `Image` API * fix double rendering * `SizeHint::Original` -> `Scale` + remove `Option` wrapper * Update crates/egui/src/load.rs Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com> * remove special `None` value for `forget` * Update crates/egui/src/load.rs Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com> * add more examples to `ui.image` + add `include_image` macro * Update crates/egui/src/ui.rs Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com> * update `menu_image_button` to use `ImageSource` * `OrderedFloat::get` -> `into_inner` * derive `Eq` on `SizedTexture` * add `id` to loaders + `is_installed` check * move `images` to demo + simplify `images` example * log trace when installing loaders * fix lint * fix doc link * add more documentation * more `egui_extras::loaders` docs * Update examples/images/src/main.rs Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com> * update `images` example screenshots + readme * remove unused `rfd` from `images` example * Update crates/egui_extras/src/loaders/ehttp_loader.rs Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com> * add `must_use` on `Image` and `RawImage` * document `loaders::install` multiple call safety * Update crates/egui_extras/Cargo.toml Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com> * reshuffle `is_loader_installed` * make `include_image` produce `ImageSource` + update docs * update `include_image` docs * remove `None` mentions from loader `forget` * inline `From` texture id + size for `SizedTexture` * add warning about statically known path * change image load error + use in image button * add `.size()` to `Image` * Update crates/egui_demo_app/Cargo.toml Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com> * add explanations to image viewer ui --------- Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
|
|
@ -1083,16 +1083,6 @@ version = "1.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
|
||||
|
||||
[[package]]
|
||||
name = "download_image"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui_extras",
|
||||
"env_logger",
|
||||
"image",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clonable"
|
||||
version = "0.9.0"
|
||||
|
|
@ -1231,6 +1221,7 @@ dependencies = [
|
|||
"image",
|
||||
"log",
|
||||
"poll-promise",
|
||||
"rfd",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
|
|
@ -1264,6 +1255,7 @@ dependencies = [
|
|||
"ehttp",
|
||||
"image",
|
||||
"log",
|
||||
"mime_guess",
|
||||
"puffin",
|
||||
"resvg",
|
||||
"serde",
|
||||
|
|
@ -2026,6 +2018,16 @@ dependencies = [
|
|||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "images"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui_extras",
|
||||
"env_logger",
|
||||
"image",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "imagesize"
|
||||
version = "0.10.1"
|
||||
|
|
@ -2310,6 +2312,22 @@ dependencies = [
|
|||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
|
|
@ -2998,16 +3016,6 @@ dependencies = [
|
|||
"usvg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "retained_image"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui_extras",
|
||||
"env_logger",
|
||||
"image",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.11.4"
|
||||
|
|
@ -3408,15 +3416,6 @@ version = "0.10.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "svg"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui_extras",
|
||||
"env_logger",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "svgtypes"
|
||||
version = "0.8.2"
|
||||
|
|
@ -3747,6 +3746,15 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.13"
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 45 KiB |
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::load::Bytes;
|
||||
use crate::load::SizedTexture;
|
||||
use crate::{
|
||||
animation_manager::AnimationManager, data::output::PlatformOutput, frame_state::FrameState,
|
||||
input_state::*, layers::GraphicLayers, memory::Options, os::OperatingSystem,
|
||||
input_state::*, layers::GraphicLayers, load::Loaders, memory::Options, os::OperatingSystem,
|
||||
output::FullOutput, util::IdTypeMap, TextureHandle, *,
|
||||
};
|
||||
use epaint::{mutex::*, stats::*, text::Fonts, TessellationOptions, *};
|
||||
|
|
@ -167,7 +169,7 @@ struct ContextImpl {
|
|||
#[cfg(feature = "accesskit")]
|
||||
accesskit_node_classes: accesskit::NodeClassSet,
|
||||
|
||||
loaders: load::Loaders,
|
||||
loaders: Arc<Loaders>,
|
||||
}
|
||||
|
||||
impl ContextImpl {
|
||||
|
|
@ -1143,7 +1145,7 @@ impl Context {
|
|||
/// });
|
||||
///
|
||||
/// // Show the image:
|
||||
/// ui.image(texture, texture.size_vec2());
|
||||
/// ui.raw_image((texture.id(), texture.size_vec2()));
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
|
|
@ -1689,14 +1691,15 @@ impl Context {
|
|||
let mut size = vec2(w as f32, h as f32);
|
||||
size *= (max_preview_size.x / size.x).min(1.0);
|
||||
size *= (max_preview_size.y / size.y).min(1.0);
|
||||
ui.image(texture_id, size).on_hover_ui(|ui| {
|
||||
// show larger on hover
|
||||
let max_size = 0.5 * ui.ctx().screen_rect().size();
|
||||
let mut size = vec2(w as f32, h as f32);
|
||||
size *= max_size.x / size.x.max(max_size.x);
|
||||
size *= max_size.y / size.y.max(max_size.y);
|
||||
ui.image(texture_id, size);
|
||||
});
|
||||
ui.raw_image(SizedTexture::new(texture_id, size))
|
||||
.on_hover_ui(|ui| {
|
||||
// show larger on hover
|
||||
let max_size = 0.5 * ui.ctx().screen_rect().size();
|
||||
let mut size = vec2(w as f32, h as f32);
|
||||
size *= max_size.x / size.x.max(max_size.x);
|
||||
size *= max_size.y / size.y.max(max_size.y);
|
||||
ui.raw_image(SizedTexture::new(texture_id, size));
|
||||
});
|
||||
|
||||
ui.label(format!("{w} x {h}"));
|
||||
ui.label(format!("{:.3} MB", meta.bytes_used() as f64 * 1e-6));
|
||||
|
|
@ -1907,60 +1910,90 @@ impl Context {
|
|||
impl Context {
|
||||
/// Associate some static bytes with a `uri`.
|
||||
///
|
||||
/// The same `uri` may be passed to [`Ui::image2`] later to load the bytes as an image.
|
||||
pub fn include_static_bytes(&self, uri: &'static str, bytes: &'static [u8]) {
|
||||
self.read(|ctx| ctx.loaders.include.insert_static(uri, bytes));
|
||||
/// The same `uri` may be passed to [`Ui::image`] later to load the bytes as an image.
|
||||
pub fn include_bytes(&self, uri: &'static str, bytes: impl Into<Bytes>) {
|
||||
self.loaders().include.insert(uri, bytes.into());
|
||||
}
|
||||
|
||||
/// Associate some bytes with a `uri`.
|
||||
///
|
||||
/// The same `uri` may be passed to [`Ui::image2`] later to load the bytes as an image.
|
||||
pub fn include_bytes(&self, uri: &'static str, bytes: impl Into<Arc<[u8]>>) {
|
||||
self.read(|ctx| ctx.loaders.include.insert_shared(uri, bytes));
|
||||
/// Returns `true` if the chain of bytes, image, or texture loaders
|
||||
/// contains a loader with the given `id`.
|
||||
pub fn is_loader_installed(&self, id: &str) -> bool {
|
||||
let loaders = self.loaders();
|
||||
|
||||
let in_bytes = loaders.bytes.lock().iter().any(|loader| loader.id() == id);
|
||||
let in_image = loaders.image.lock().iter().any(|loader| loader.id() == id);
|
||||
let in_texture = loaders
|
||||
.texture
|
||||
.lock()
|
||||
.iter()
|
||||
.any(|loader| loader.id() == id);
|
||||
|
||||
in_bytes || in_image || in_texture
|
||||
}
|
||||
|
||||
/// Append an entry onto the chain of bytes loaders.
|
||||
///
|
||||
/// See [`load`] for more information.
|
||||
pub fn add_bytes_loader(&self, loader: Arc<dyn load::BytesLoader + Send + Sync + 'static>) {
|
||||
self.write(|ctx| ctx.loaders.bytes.push(loader));
|
||||
self.loaders().bytes.lock().push(loader);
|
||||
}
|
||||
|
||||
/// Append an entry onto the chain of image loaders.
|
||||
///
|
||||
/// See [`load`] for more information.
|
||||
pub fn add_image_loader(&self, loader: Arc<dyn load::ImageLoader + Send + Sync + 'static>) {
|
||||
self.write(|ctx| ctx.loaders.image.push(loader));
|
||||
self.loaders().image.lock().push(loader);
|
||||
}
|
||||
|
||||
/// Append an entry onto the chain of texture loaders.
|
||||
///
|
||||
/// See [`load`] for more information.
|
||||
pub fn add_texture_loader(&self, loader: Arc<dyn load::TextureLoader + Send + Sync + 'static>) {
|
||||
self.write(|ctx| ctx.loaders.texture.push(loader));
|
||||
self.loaders().texture.lock().push(loader);
|
||||
}
|
||||
|
||||
/// Release all memory and textures related to the given image URI.
|
||||
///
|
||||
/// If you attempt to load the image again, it will be reloaded from scratch.
|
||||
pub fn forget_image(&self, uri: &str) {
|
||||
self.write(|ctx| {
|
||||
use crate::load::BytesLoader as _;
|
||||
use load::BytesLoader as _;
|
||||
|
||||
ctx.loaders.include.forget(uri);
|
||||
crate::profile_function!();
|
||||
|
||||
for loader in &ctx.loaders.bytes {
|
||||
loader.forget(uri);
|
||||
}
|
||||
let loaders = self.loaders();
|
||||
|
||||
for loader in &ctx.loaders.image {
|
||||
loader.forget(uri);
|
||||
}
|
||||
loaders.include.forget(uri);
|
||||
for loader in loaders.bytes.lock().iter() {
|
||||
loader.forget(uri);
|
||||
}
|
||||
for loader in loaders.image.lock().iter() {
|
||||
loader.forget(uri);
|
||||
}
|
||||
for loader in loaders.texture.lock().iter() {
|
||||
loader.forget(uri);
|
||||
}
|
||||
}
|
||||
|
||||
for loader in &ctx.loaders.texture {
|
||||
loader.forget(uri);
|
||||
}
|
||||
});
|
||||
/// Release all memory and textures related to images used in [`Ui::image`] or [`Image`].
|
||||
///
|
||||
/// If you attempt to load any images again, they will be reloaded from scratch.
|
||||
pub fn forget_all_images(&self) {
|
||||
use load::BytesLoader as _;
|
||||
|
||||
crate::profile_function!();
|
||||
|
||||
let loaders = self.loaders();
|
||||
|
||||
loaders.include.forget_all();
|
||||
for loader in loaders.bytes.lock().iter() {
|
||||
loader.forget_all();
|
||||
}
|
||||
for loader in loaders.image.lock().iter() {
|
||||
loader.forget_all();
|
||||
}
|
||||
for loader in loaders.texture.lock().iter() {
|
||||
loader.forget_all();
|
||||
}
|
||||
}
|
||||
|
||||
/// Try loading the bytes from the given uri using any available bytes loaders.
|
||||
|
|
@ -1977,11 +2010,14 @@ impl Context {
|
|||
/// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`.
|
||||
/// - [`LoadError::Custom`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed.
|
||||
///
|
||||
/// ⚠ May deadlock if called from within a `BytesLoader`!
|
||||
///
|
||||
/// [not_supported]: crate::load::LoadError::NotSupported
|
||||
/// [custom]: crate::load::LoadError::Custom
|
||||
pub fn try_load_bytes(&self, uri: &str) -> load::BytesLoadResult {
|
||||
let loaders = self.loaders();
|
||||
for loader in &loaders.bytes {
|
||||
crate::profile_function!();
|
||||
|
||||
for loader in self.loaders().bytes.lock().iter() {
|
||||
match loader.load(self, uri) {
|
||||
Err(load::LoadError::NotSupported) => continue,
|
||||
result => return result,
|
||||
|
|
@ -2005,11 +2041,14 @@ impl Context {
|
|||
/// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`.
|
||||
/// - [`LoadError::Custom`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed.
|
||||
///
|
||||
/// ⚠ May deadlock if called from within an `ImageLoader`!
|
||||
///
|
||||
/// [not_supported]: crate::load::LoadError::NotSupported
|
||||
/// [custom]: crate::load::LoadError::Custom
|
||||
pub fn try_load_image(&self, uri: &str, size_hint: load::SizeHint) -> load::ImageLoadResult {
|
||||
let loaders = self.loaders();
|
||||
for loader in &loaders.image {
|
||||
crate::profile_function!();
|
||||
|
||||
for loader in self.loaders().image.lock().iter() {
|
||||
match loader.load(self, uri, size_hint) {
|
||||
Err(load::LoadError::NotSupported) => continue,
|
||||
result => return result,
|
||||
|
|
@ -2033,6 +2072,8 @@ impl Context {
|
|||
/// - [`LoadError::NotSupported`][not_supported] if none of the registered loaders support loading the given `uri`.
|
||||
/// - [`LoadError::Custom`][custom] if one of the loaders _does_ support loading the `uri`, but the loading process failed.
|
||||
///
|
||||
/// ⚠ May deadlock if called from within a `TextureLoader`!
|
||||
///
|
||||
/// [not_supported]: crate::load::LoadError::NotSupported
|
||||
/// [custom]: crate::load::LoadError::Custom
|
||||
pub fn try_load_texture(
|
||||
|
|
@ -2041,9 +2082,9 @@ impl Context {
|
|||
texture_options: TextureOptions,
|
||||
size_hint: load::SizeHint,
|
||||
) -> load::TextureLoadResult {
|
||||
let loaders = self.loaders();
|
||||
crate::profile_function!();
|
||||
|
||||
for loader in &loaders.texture {
|
||||
for loader in self.loaders().texture.lock().iter() {
|
||||
match loader.load(self, uri, texture_options, size_hint) {
|
||||
Err(load::LoadError::NotSupported) => continue,
|
||||
result => return result,
|
||||
|
|
@ -2053,9 +2094,9 @@ impl Context {
|
|||
Err(load::LoadError::NotSupported)
|
||||
}
|
||||
|
||||
fn loaders(&self) -> load::Loaders {
|
||||
fn loaders(&self) -> Arc<Loaders> {
|
||||
crate::profile_function!();
|
||||
self.read(|this| this.loaders.clone()) // TODO(emilk): something less slow
|
||||
self.read(|this| this.loaders.clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@
|
|||
//! ui.separator();
|
||||
//!
|
||||
//! # let my_image = egui::TextureId::default();
|
||||
//! ui.image(my_image, [640.0, 480.0]);
|
||||
//! ui.raw_image((my_image, egui::Vec2::new(640.0, 480.0)));
|
||||
//!
|
||||
//! ui.collapsing("Click to see what is hidden!", |ui| {
|
||||
//! ui.label("Not much, as it turns out");
|
||||
|
|
@ -424,6 +424,28 @@ pub fn warn_if_debug_build(ui: &mut crate::Ui) {
|
|||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Include an image in the binary.
|
||||
///
|
||||
/// This is a wrapper over `include_bytes!`, and behaves in the same way.
|
||||
///
|
||||
/// It produces an [`ImageSource`] which can be used directly in [`Ui::image`] or [`Image::new`]:
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// ui.image(egui::include_image!("../assets/ferris.png"));
|
||||
/// ui.add(
|
||||
/// egui::Image::new(egui::include_image!("../assets/ferris.png"))
|
||||
/// .rounding(egui::Rounding::same(6.0))
|
||||
/// );
|
||||
/// # });
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! include_image {
|
||||
($path: literal) => {
|
||||
$crate::ImageSource::Bytes($path, $crate::load::Bytes::Static(include_bytes!($path)))
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a [`Hyperlink`](crate::Hyperlink) to the current [`file!()`] (and line) on Github
|
||||
///
|
||||
/// ```
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
//! Types and traits related to image loading.
|
||||
//! # Image loading
|
||||
//!
|
||||
//! If you just want to load some images, see [`egui_extras`](https://crates.io/crates/egui_extras/),
|
||||
//! which contains reasonable default implementations of these traits. You can get started quickly
|
||||
//! using [`egui_extras::loaders::install`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html).
|
||||
//! If you just want to display some images, [`egui_extras`](https://crates.io/crates/egui_extras/)
|
||||
//! will get you up and running quickly with its reasonable default implementations of the traits described below.
|
||||
//!
|
||||
//! 1. Add [`egui_extras`](https://crates.io/crates/egui_extras/) as a dependency with the `all-loaders` feature.
|
||||
//! 2. Add a call to [`egui_extras::loaders::install`](https://docs.rs/egui_extras/latest/egui_extras/loaders/fn.install.html)
|
||||
//! in your app's setup code.
|
||||
//! 3. Use [`Ui::image`][`crate::ui::Ui::image`] with some [`ImageSource`][`crate::ImageSource`].
|
||||
//!
|
||||
//! ## Loading process
|
||||
//!
|
||||
|
|
@ -14,13 +18,13 @@
|
|||
//! The different kinds of loaders represent different layers in the loading process:
|
||||
//!
|
||||
//! ```text,ignore
|
||||
//! ui.image2("file://image.png")
|
||||
//! └► ctx.try_load_texture("file://image.png", ...)
|
||||
//! └► TextureLoader::load("file://image.png", ...)
|
||||
//! └► ctx.try_load_image("file://image.png", ...)
|
||||
//! └► ImageLoader::load("file://image.png", ...)
|
||||
//! └► ctx.try_load_bytes("file://image.png", ...)
|
||||
//! └► BytesLoader::load("file://image.png", ...)
|
||||
//! ui.image("file://image.png")
|
||||
//! └► Context::try_load_texture
|
||||
//! └► TextureLoader::load
|
||||
//! └► Context::try_load_image
|
||||
//! └► ImageLoader::load
|
||||
//! └► Context::try_load_bytes
|
||||
//! └► BytesLoader::load
|
||||
//! ```
|
||||
//!
|
||||
//! As each layer attempts to load the URI, it first asks the layer below it
|
||||
|
|
@ -51,11 +55,15 @@
|
|||
use crate::Context;
|
||||
use ahash::HashMap;
|
||||
use epaint::mutex::Mutex;
|
||||
use epaint::util::FloatOrd;
|
||||
use epaint::util::OrderedFloat;
|
||||
use epaint::TextureHandle;
|
||||
use epaint::{textures::TextureOptions, ColorImage, TextureId, Vec2};
|
||||
use std::fmt::Debug;
|
||||
use std::ops::Deref;
|
||||
use std::{error::Error as StdError, fmt::Display, sync::Arc};
|
||||
|
||||
/// Represents a failed attempt at loading an image.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum LoadError {
|
||||
/// This loader does not support this protocol or image format.
|
||||
|
|
@ -85,11 +93,10 @@ pub type Result<T, E = LoadError> = std::result::Result<T, E>;
|
|||
/// All variants will preserve the original aspect ratio.
|
||||
///
|
||||
/// Similar to `usvg::FitTo`.
|
||||
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum SizeHint {
|
||||
/// Keep original size.
|
||||
#[default]
|
||||
Original,
|
||||
/// Scale original size by some factor.
|
||||
Scale(OrderedFloat<f32>),
|
||||
|
||||
/// Scale to width.
|
||||
Width(u32),
|
||||
|
|
@ -101,22 +108,36 @@ pub enum SizeHint {
|
|||
Size(u32, u32),
|
||||
}
|
||||
|
||||
impl Default for SizeHint {
|
||||
fn default() -> Self {
|
||||
Self::Scale(1.0.ord())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec2> for SizeHint {
|
||||
fn from(value: Vec2) -> Self {
|
||||
Self::Size(value.x.round() as u32, value.y.round() as u32)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: API for querying bytes caches in each loader
|
||||
|
||||
pub type Size = [usize; 2];
|
||||
|
||||
/// Represents a byte buffer.
|
||||
///
|
||||
/// This is essentially `Cow<'static, [u8]>` but with the `Owned` variant being an `Arc`.
|
||||
#[derive(Clone)]
|
||||
pub enum Bytes {
|
||||
Static(&'static [u8]),
|
||||
Shared(Arc<[u8]>),
|
||||
}
|
||||
|
||||
impl Debug for Bytes {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Static(arg0) => f.debug_tuple("Static").field(&arg0.len()).finish(),
|
||||
Self::Shared(arg0) => f.debug_tuple("Shared").field(&arg0.len()).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static [u8]> for Bytes {
|
||||
#[inline]
|
||||
fn from(value: &'static [u8]) -> Self {
|
||||
|
|
@ -124,6 +145,13 @@ impl From<&'static [u8]> for Bytes {
|
|||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> From<&'static [u8; N]> for Bytes {
|
||||
#[inline]
|
||||
fn from(value: &'static [u8; N]) -> Self {
|
||||
Bytes::Static(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<[u8]>> for Bytes {
|
||||
#[inline]
|
||||
fn from(value: Arc<[u8]>) -> Self {
|
||||
|
|
@ -131,6 +159,13 @@ impl From<Arc<[u8]>> for Bytes {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Bytes {
|
||||
#[inline]
|
||||
fn from(value: Vec<u8>) -> Self {
|
||||
Bytes::Shared(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Bytes {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
|
|
@ -150,27 +185,59 @@ impl Deref for Bytes {
|
|||
}
|
||||
}
|
||||
|
||||
/// Represents bytes which are currently being loaded.
|
||||
///
|
||||
/// This is similar to [`std::task::Poll`], but the `Pending` variant
|
||||
/// contains an optional `size`, which may be used during layout to
|
||||
/// pre-allocate space the image.
|
||||
#[derive(Clone)]
|
||||
pub enum BytesPoll {
|
||||
/// Bytes are being loaded.
|
||||
Pending {
|
||||
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
||||
size: Option<Size>,
|
||||
size: Option<Vec2>,
|
||||
},
|
||||
|
||||
/// Bytes are loaded.
|
||||
Ready {
|
||||
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
||||
size: Option<Size>,
|
||||
size: Option<Vec2>,
|
||||
|
||||
/// File contents, e.g. the contents of a `.png`.
|
||||
bytes: Bytes,
|
||||
|
||||
/// Mime type of the content, e.g. `image/png`.
|
||||
///
|
||||
/// Set if known (e.g. from `Content-Type` HTTP header).
|
||||
mime: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Used to get a unique ID when implementing one of the loader traits: [`BytesLoader::id`], [`ImageLoader::id`], and [`TextureLoader::id`].
|
||||
///
|
||||
/// This just expands to `module_path!()` concatenated with the given type name.
|
||||
#[macro_export]
|
||||
macro_rules! generate_loader_id {
|
||||
($ty:ident) => {
|
||||
concat!(module_path!(), "::", stringify!($ty))
|
||||
};
|
||||
}
|
||||
pub use crate::generate_loader_id;
|
||||
|
||||
pub type BytesLoadResult = Result<BytesPoll>;
|
||||
|
||||
/// Represents a loader capable of loading raw unstructured bytes.
|
||||
///
|
||||
/// It should also provide any subsequent loaders a hint for what the bytes may
|
||||
/// represent using [`BytesPoll::Ready::mime`], if it can be inferred.
|
||||
///
|
||||
/// Implementations are expected to cache at least each `URI`.
|
||||
pub trait BytesLoader {
|
||||
/// Unique ID of this loader.
|
||||
///
|
||||
/// To reduce the chance of collisions, use [`generate_loader_id`] for this.
|
||||
fn id(&self) -> &str;
|
||||
|
||||
/// Try loading the bytes from the given uri.
|
||||
///
|
||||
/// Implementations should call `ctx.request_repaint` to wake up the ui
|
||||
|
|
@ -191,6 +258,12 @@ pub trait BytesLoader {
|
|||
/// so that it may be fully reloaded.
|
||||
fn forget(&self, uri: &str);
|
||||
|
||||
/// Forget all URIs ever given to this loader.
|
||||
///
|
||||
/// If the loader caches any URIs, the entire cache should be cleared,
|
||||
/// so that all of them may be fully reloaded.
|
||||
fn forget_all(&self);
|
||||
|
||||
/// Implementations may use this to perform work at the end of a frame,
|
||||
/// such as evicting unused entries from a cache.
|
||||
fn end_frame(&self, frame_index: usize) {
|
||||
|
|
@ -201,12 +274,17 @@ pub trait BytesLoader {
|
|||
fn byte_size(&self) -> usize;
|
||||
}
|
||||
|
||||
/// Represents an image which is currently being loaded.
|
||||
///
|
||||
/// This is similar to [`std::task::Poll`], but the `Pending` variant
|
||||
/// contains an optional `size`, which may be used during layout to
|
||||
/// pre-allocate space the image.
|
||||
#[derive(Clone)]
|
||||
pub enum ImagePoll {
|
||||
/// Image is loading.
|
||||
Pending {
|
||||
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
||||
size: Option<Size>,
|
||||
size: Option<Vec2>,
|
||||
},
|
||||
|
||||
/// Image is loaded.
|
||||
|
|
@ -215,7 +293,18 @@ pub enum ImagePoll {
|
|||
|
||||
pub type ImageLoadResult = Result<ImagePoll>;
|
||||
|
||||
/// Represents a loader capable of loading a raw image.
|
||||
///
|
||||
/// Implementations are expected to cache at least each `URI`.
|
||||
pub trait ImageLoader {
|
||||
/// Unique ID of this loader.
|
||||
///
|
||||
/// To reduce the chance of collisions, include `module_path!()` as part of this ID.
|
||||
///
|
||||
/// For example: `concat!(module_path!(), "::MyLoader")`
|
||||
/// for `my_crate::my_loader::MyLoader`.
|
||||
fn id(&self) -> &str;
|
||||
|
||||
/// Try loading the image from the given uri.
|
||||
///
|
||||
/// Implementations should call `ctx.request_repaint` to wake up the ui
|
||||
|
|
@ -236,6 +325,12 @@ pub trait ImageLoader {
|
|||
/// so that it may be fully reloaded.
|
||||
fn forget(&self, uri: &str);
|
||||
|
||||
/// Forget all URIs ever given to this loader.
|
||||
///
|
||||
/// If the loader caches any URIs, the entire cache should be cleared,
|
||||
/// so that all of them may be fully reloaded.
|
||||
fn forget_all(&self);
|
||||
|
||||
/// Implementations may use this to perform work at the end of a frame,
|
||||
/// such as evicting unused entries from a cache.
|
||||
fn end_frame(&self, frame_index: usize) {
|
||||
|
|
@ -247,27 +342,49 @@ pub trait ImageLoader {
|
|||
}
|
||||
|
||||
/// A texture with a known size.
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SizedTexture {
|
||||
pub id: TextureId,
|
||||
pub size: Size,
|
||||
pub size: Vec2,
|
||||
}
|
||||
|
||||
impl SizedTexture {
|
||||
/// Create a [`SizedTexture`] from a texture `id` with a specific `size`.
|
||||
pub fn new(id: impl Into<TextureId>, size: impl Into<Vec2>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
size: size.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the [id][`SizedTexture::id`] and [size][`SizedTexture::size`] from a [`TextureHandle`].
|
||||
pub fn from_handle(handle: &TextureHandle) -> Self {
|
||||
let size = handle.size();
|
||||
Self {
|
||||
id: handle.id(),
|
||||
size: handle.size(),
|
||||
size: Vec2::new(size[0] as f32, size[1] as f32),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(TextureId, Vec2)> for SizedTexture {
|
||||
#[inline]
|
||||
fn from((id, size): (TextureId, Vec2)) -> Self {
|
||||
SizedTexture { id, size }
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a texture is currently being loaded.
|
||||
///
|
||||
/// This is similar to [`std::task::Poll`], but the `Pending` variant
|
||||
/// contains an optional `size`, which may be used during layout to
|
||||
/// pre-allocate space the image.
|
||||
#[derive(Clone)]
|
||||
pub enum TexturePoll {
|
||||
/// Texture is loading.
|
||||
Pending {
|
||||
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
|
||||
size: Option<Size>,
|
||||
size: Option<Vec2>,
|
||||
},
|
||||
|
||||
/// Texture is loaded.
|
||||
|
|
@ -276,7 +393,18 @@ pub enum TexturePoll {
|
|||
|
||||
pub type TextureLoadResult = Result<TexturePoll>;
|
||||
|
||||
/// Represents a loader capable of loading a full texture.
|
||||
///
|
||||
/// Implementations are expected to cache each combination of `(URI, TextureOptions)`.
|
||||
pub trait TextureLoader {
|
||||
/// Unique ID of this loader.
|
||||
///
|
||||
/// To reduce the chance of collisions, include `module_path!()` as part of this ID.
|
||||
///
|
||||
/// For example: `concat!(module_path!(), "::MyLoader")`
|
||||
/// for `my_crate::my_loader::MyLoader`.
|
||||
fn id(&self) -> &str;
|
||||
|
||||
/// Try loading the texture from the given uri.
|
||||
///
|
||||
/// Implementations should call `ctx.request_repaint` to wake up the ui
|
||||
|
|
@ -303,6 +431,12 @@ pub trait TextureLoader {
|
|||
/// so that it may be fully reloaded.
|
||||
fn forget(&self, uri: &str);
|
||||
|
||||
/// Forget all URIs ever given to this loader.
|
||||
///
|
||||
/// If the loader caches any URIs, the entire cache should be cleared,
|
||||
/// so that all of them may be fully reloaded.
|
||||
fn forget_all(&self);
|
||||
|
||||
/// Implementations may use this to perform work at the end of a frame,
|
||||
/// such as evicting unused entries from a cache.
|
||||
fn end_frame(&self, frame_index: usize) {
|
||||
|
|
@ -319,25 +453,23 @@ pub(crate) struct DefaultBytesLoader {
|
|||
}
|
||||
|
||||
impl DefaultBytesLoader {
|
||||
pub(crate) fn insert_static(&self, uri: &'static str, bytes: &'static [u8]) {
|
||||
self.cache
|
||||
.lock()
|
||||
.entry(uri)
|
||||
.or_insert_with(|| Bytes::Static(bytes));
|
||||
}
|
||||
|
||||
pub(crate) fn insert_shared(&self, uri: &'static str, bytes: impl Into<Arc<[u8]>>) {
|
||||
self.cache
|
||||
.lock()
|
||||
.entry(uri)
|
||||
.or_insert_with(|| Bytes::Shared(bytes.into()));
|
||||
pub(crate) fn insert(&self, uri: &'static str, bytes: impl Into<Bytes>) {
|
||||
self.cache.lock().entry(uri).or_insert_with(|| bytes.into());
|
||||
}
|
||||
}
|
||||
|
||||
impl BytesLoader for DefaultBytesLoader {
|
||||
fn id(&self) -> &str {
|
||||
generate_loader_id!(DefaultBytesLoader)
|
||||
}
|
||||
|
||||
fn load(&self, _: &Context, uri: &str) -> BytesLoadResult {
|
||||
match self.cache.lock().get(uri).cloned() {
|
||||
Some(bytes) => Ok(BytesPoll::Ready { size: None, bytes }),
|
||||
Some(bytes) => Ok(BytesPoll::Ready {
|
||||
size: None,
|
||||
bytes,
|
||||
mime: None,
|
||||
}),
|
||||
None => Err(LoadError::NotSupported),
|
||||
}
|
||||
}
|
||||
|
|
@ -346,6 +478,10 @@ impl BytesLoader for DefaultBytesLoader {
|
|||
let _ = self.cache.lock().remove(uri);
|
||||
}
|
||||
|
||||
fn forget_all(&self) {
|
||||
self.cache.lock().clear();
|
||||
}
|
||||
|
||||
fn byte_size(&self) -> usize {
|
||||
self.cache.lock().values().map(|bytes| bytes.len()).sum()
|
||||
}
|
||||
|
|
@ -357,6 +493,10 @@ struct DefaultTextureLoader {
|
|||
}
|
||||
|
||||
impl TextureLoader for DefaultTextureLoader {
|
||||
fn id(&self) -> &str {
|
||||
generate_loader_id!(DefaultTextureLoader)
|
||||
}
|
||||
|
||||
fn load(
|
||||
&self,
|
||||
ctx: &Context,
|
||||
|
|
@ -385,6 +525,10 @@ impl TextureLoader for DefaultTextureLoader {
|
|||
self.cache.lock().retain(|(u, _), _| u != uri);
|
||||
}
|
||||
|
||||
fn forget_all(&self) {
|
||||
self.cache.lock().clear();
|
||||
}
|
||||
|
||||
fn end_frame(&self, _: usize) {}
|
||||
|
||||
fn byte_size(&self) -> usize {
|
||||
|
|
@ -396,22 +540,26 @@ impl TextureLoader for DefaultTextureLoader {
|
|||
}
|
||||
}
|
||||
|
||||
type BytesLoaderImpl = Arc<dyn BytesLoader + Send + Sync + 'static>;
|
||||
type ImageLoaderImpl = Arc<dyn ImageLoader + Send + Sync + 'static>;
|
||||
type TextureLoaderImpl = Arc<dyn TextureLoader + Send + Sync + 'static>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Loaders {
|
||||
pub include: Arc<DefaultBytesLoader>,
|
||||
pub bytes: Vec<Arc<dyn BytesLoader + Send + Sync + 'static>>,
|
||||
pub image: Vec<Arc<dyn ImageLoader + Send + Sync + 'static>>,
|
||||
pub texture: Vec<Arc<dyn TextureLoader + Send + Sync + 'static>>,
|
||||
pub bytes: Mutex<Vec<BytesLoaderImpl>>,
|
||||
pub image: Mutex<Vec<ImageLoaderImpl>>,
|
||||
pub texture: Mutex<Vec<TextureLoaderImpl>>,
|
||||
}
|
||||
|
||||
impl Default for Loaders {
|
||||
fn default() -> Self {
|
||||
let include = Arc::new(DefaultBytesLoader::default());
|
||||
Self {
|
||||
bytes: vec![include.clone()],
|
||||
image: Vec::new(),
|
||||
bytes: Mutex::new(vec![include.clone()]),
|
||||
image: Mutex::new(Vec::new()),
|
||||
// By default we only include `DefaultTextureLoader`.
|
||||
texture: vec![Arc::new(DefaultTextureLoader::default())],
|
||||
texture: Mutex::new(vec![Arc::new(DefaultTextureLoader::default())]),
|
||||
include,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ pub fn menu_button<R>(
|
|||
/// Returns `None` if the menu is not open.
|
||||
pub fn menu_image_button<R>(
|
||||
ui: &mut Ui,
|
||||
image_button: ImageButton,
|
||||
image_button: ImageButton<'_>,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
stationary_menu_image_impl(ui, image_button, Box::new(add_contents))
|
||||
|
|
@ -201,7 +201,7 @@ fn stationary_menu_impl<'c, R>(
|
|||
/// Responds to primary clicks.
|
||||
fn stationary_menu_image_impl<'c, R>(
|
||||
ui: &mut Ui,
|
||||
image_button: ImageButton,
|
||||
image_button: ImageButton<'_>,
|
||||
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
let bar_id = ui.id();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use std::sync::Arc;
|
|||
|
||||
use epaint::mutex::RwLock;
|
||||
|
||||
use crate::load::SizedTexture;
|
||||
use crate::{
|
||||
containers::*, ecolor::*, epaint::text::Fonts, layout::*, menu::MenuState, placer::Placer,
|
||||
util::IdTypeMap, widgets::*, *,
|
||||
|
|
@ -1558,39 +1559,6 @@ impl Ui {
|
|||
response
|
||||
}
|
||||
|
||||
/// Show an image here with the given size.
|
||||
///
|
||||
/// In order to display an image you must first acquire a [`TextureHandle`].
|
||||
/// This is best done with [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html) or [`Context::load_texture`].
|
||||
///
|
||||
/// ```
|
||||
/// struct MyImage {
|
||||
/// texture: Option<egui::TextureHandle>,
|
||||
/// }
|
||||
///
|
||||
/// impl MyImage {
|
||||
/// fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
/// let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
|
||||
/// // Load the texture only once.
|
||||
/// ui.ctx().load_texture(
|
||||
/// "my-image",
|
||||
/// egui::ColorImage::example(),
|
||||
/// Default::default()
|
||||
/// )
|
||||
/// });
|
||||
///
|
||||
/// // Show the image:
|
||||
/// ui.image(texture, texture.size_vec2());
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// See also [`crate::Image`] and [`crate::ImageButton`].
|
||||
#[inline]
|
||||
pub fn image(&mut self, texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Response {
|
||||
Image::new(texture_id, size).ui(self)
|
||||
}
|
||||
|
||||
/// Show an image available at the given `uri`.
|
||||
///
|
||||
/// ⚠ This will do nothing unless you install some image loaders first!
|
||||
|
|
@ -1600,14 +1568,61 @@ impl Ui {
|
|||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// ui.image2("file://ferris.svg");
|
||||
/// ui.image("https://picsum.photos/480");
|
||||
/// ui.image("file://assets/ferris.png");
|
||||
/// ui.image(egui::include_image!("../assets/ferris.png"));
|
||||
/// ui.add(
|
||||
/// egui::Image::new(egui::include_image!("../assets/ferris.png"))
|
||||
/// .rounding(egui::Rounding::same(6.0))
|
||||
/// );
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// See also [`crate::Image2`] and [`crate::ImageSource`].
|
||||
/// Note: Prefer `include_image` as a source if you're loading an image
|
||||
/// from a file with a statically known path, unless you really want to
|
||||
/// load it at runtime instead!
|
||||
///
|
||||
/// See also [`crate::Image`], [`crate::ImageSource`] and [`Self::raw_image`].
|
||||
#[inline]
|
||||
pub fn image2<'a>(&mut self, source: impl Into<ImageSource<'a>>) -> Response {
|
||||
Image2::new(source.into()).ui(self)
|
||||
pub fn image<'a>(&mut self, source: impl Into<ImageSource<'a>>) -> Response {
|
||||
Image::new(source.into()).ui(self)
|
||||
}
|
||||
|
||||
/// Show an image created from a sized texture.
|
||||
///
|
||||
/// You may use this method over [`Ui::image`] if you already have a [`TextureHandle`]
|
||||
/// or a [`SizedTexture`].
|
||||
///
|
||||
/// ```
|
||||
/// # egui::__run_test_ui(|ui| {
|
||||
/// struct MyImage {
|
||||
/// texture: Option<egui::TextureHandle>,
|
||||
/// }
|
||||
///
|
||||
/// impl MyImage {
|
||||
/// fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
/// let texture = self
|
||||
/// .texture
|
||||
/// .get_or_insert_with(|| {
|
||||
/// // Load the texture only once.
|
||||
/// ui.ctx().load_texture(
|
||||
/// "my-image",
|
||||
/// egui::ColorImage::example(),
|
||||
/// Default::default()
|
||||
/// )
|
||||
/// });
|
||||
///
|
||||
/// // Show the image:
|
||||
/// ui.raw_image((texture.id(), texture.size_vec2()));
|
||||
/// }
|
||||
/// }
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// See also [`crate::RawImage`].
|
||||
#[inline]
|
||||
pub fn raw_image(&mut self, texture: impl Into<SizedTexture>) -> Response {
|
||||
RawImage::new(texture).ui(self)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2206,15 +2221,9 @@ impl Ui {
|
|||
/// If called from within a menu this will instead create a button for a sub-menu.
|
||||
///
|
||||
/// ```ignore
|
||||
/// use egui_extras;
|
||||
/// let img = egui::include_image!("../assets/ferris.png");
|
||||
///
|
||||
/// let img = egui_extras::RetainedImage::from_svg_bytes_with_size(
|
||||
/// "rss",
|
||||
/// include_bytes!("rss.svg"),
|
||||
/// egui_extras::image::FitTo::Size(24, 24),
|
||||
/// );
|
||||
///
|
||||
/// ui.menu_image_button(img.texture_id(ctx), img.size_vec2(), |ui| {
|
||||
/// ui.menu_image_button(img, |ui| {
|
||||
/// ui.menu_button("My sub-menu", |ui| {
|
||||
/// if ui.button("Close the menu").clicked() {
|
||||
/// ui.close_menu();
|
||||
|
|
@ -2225,16 +2234,15 @@ impl Ui {
|
|||
///
|
||||
/// See also: [`Self::close_menu`] and [`Response::context_menu`].
|
||||
#[inline]
|
||||
pub fn menu_image_button<R>(
|
||||
pub fn menu_image_button<'a, R>(
|
||||
&mut self,
|
||||
texture_id: TextureId,
|
||||
image_size: impl Into<Vec2>,
|
||||
image_source: impl Into<ImageSource<'a>>,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
if let Some(menu_state) = self.menu_state.clone() {
|
||||
menu::submenu_button(self, menu_state, String::new(), add_contents)
|
||||
} else {
|
||||
menu::menu_image_button(self, ImageButton::new(texture_id, image_size), add_contents)
|
||||
menu::menu_image_button(self, ImageButton::new(image_source), add_contents)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use crate::load::SizedTexture;
|
||||
use crate::load::TexturePoll;
|
||||
use crate::*;
|
||||
|
||||
/// Clickable button with text.
|
||||
|
|
@ -32,7 +34,7 @@ pub struct Button {
|
|||
frame: Option<bool>,
|
||||
min_size: Vec2,
|
||||
rounding: Option<Rounding>,
|
||||
image: Option<widgets::Image>,
|
||||
image: Option<widgets::RawImage>,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
|
|
@ -60,7 +62,10 @@ impl Button {
|
|||
text: impl Into<WidgetText>,
|
||||
) -> Self {
|
||||
Self {
|
||||
image: Some(widgets::Image::new(texture_id, image_size)),
|
||||
image: Some(widgets::RawImage::new(SizedTexture {
|
||||
id: texture_id,
|
||||
size: image_size.into(),
|
||||
})),
|
||||
..Self::new(text)
|
||||
}
|
||||
}
|
||||
|
|
@ -161,7 +166,7 @@ impl Widget for Button {
|
|||
}
|
||||
|
||||
let mut text_wrap_width = ui.available_width() - 2.0 * button_padding.x;
|
||||
if let Some(image) = image {
|
||||
if let Some(image) = &image {
|
||||
text_wrap_width -= image.size().x + ui.spacing().icon_spacing;
|
||||
}
|
||||
if !shortcut_text.is_empty() {
|
||||
|
|
@ -173,7 +178,7 @@ impl Widget for Button {
|
|||
.then(|| shortcut_text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Button));
|
||||
|
||||
let mut desired_size = text.size();
|
||||
if let Some(image) = image {
|
||||
if let Some(image) = &image {
|
||||
desired_size.x += image.size().x + ui.spacing().icon_spacing;
|
||||
desired_size.y = desired_size.y.max(image.size().y);
|
||||
}
|
||||
|
|
@ -201,7 +206,7 @@ impl Widget for Button {
|
|||
.rect(rect.expand(visuals.expansion), rounding, fill, stroke);
|
||||
}
|
||||
|
||||
let text_pos = if let Some(image) = image {
|
||||
let text_pos = if let Some(image) = &image {
|
||||
let icon_spacing = ui.spacing().icon_spacing;
|
||||
pos2(
|
||||
rect.min.x + button_padding.x + image.size().x + icon_spacing,
|
||||
|
|
@ -226,7 +231,7 @@ impl Widget for Button {
|
|||
);
|
||||
}
|
||||
|
||||
if let Some(image) = image {
|
||||
if let Some(image) = &image {
|
||||
let image_rect = Rect::from_min_size(
|
||||
pos2(
|
||||
rect.min.x + button_padding.x,
|
||||
|
|
@ -468,17 +473,17 @@ impl Widget for RadioButton {
|
|||
/// A clickable image within a frame.
|
||||
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ImageButton {
|
||||
image: widgets::Image,
|
||||
pub struct ImageButton<'a> {
|
||||
image: Image<'a>,
|
||||
sense: Sense,
|
||||
frame: bool,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl ImageButton {
|
||||
pub fn new(texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Self {
|
||||
impl<'a> ImageButton<'a> {
|
||||
pub fn new(source: impl Into<ImageSource<'a>>) -> Self {
|
||||
Self {
|
||||
image: widgets::Image::new(texture_id, size),
|
||||
image: Image::new(source.into()),
|
||||
sense: Sense::click(),
|
||||
frame: true,
|
||||
selected: false,
|
||||
|
|
@ -515,29 +520,21 @@ impl ImageButton {
|
|||
self.sense = sense;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for ImageButton {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let Self {
|
||||
image,
|
||||
sense,
|
||||
frame,
|
||||
selected,
|
||||
} = self;
|
||||
|
||||
let padding = if frame {
|
||||
fn show(&self, ui: &mut Ui, texture: &SizedTexture) -> Response {
|
||||
let padding = if self.frame {
|
||||
// so we can see that it is a button:
|
||||
Vec2::splat(ui.spacing().button_padding.x)
|
||||
} else {
|
||||
Vec2::ZERO
|
||||
};
|
||||
let padded_size = image.size() + 2.0 * padding;
|
||||
let (rect, response) = ui.allocate_exact_size(padded_size, sense);
|
||||
|
||||
let padded_size = texture.size + 2.0 * padding;
|
||||
let (rect, response) = ui.allocate_exact_size(padded_size, self.sense);
|
||||
response.widget_info(|| WidgetInfo::new(WidgetType::ImageButton));
|
||||
|
||||
if ui.is_rect_visible(rect) {
|
||||
let (expansion, rounding, fill, stroke) = if selected {
|
||||
let (expansion, rounding, fill, stroke) = if self.selected {
|
||||
let selection = ui.visuals().selection;
|
||||
(
|
||||
Vec2::ZERO,
|
||||
|
|
@ -545,7 +542,7 @@ impl Widget for ImageButton {
|
|||
selection.bg_fill,
|
||||
selection.stroke,
|
||||
)
|
||||
} else if frame {
|
||||
} else if self.frame {
|
||||
let visuals = ui.style().interact(&response);
|
||||
let expansion = Vec2::splat(visuals.expansion);
|
||||
(
|
||||
|
|
@ -558,17 +555,19 @@ impl Widget for ImageButton {
|
|||
Default::default()
|
||||
};
|
||||
|
||||
let image = image.rounding(rounding); // apply rounding to the image
|
||||
|
||||
// Draw frame background (for transparent images):
|
||||
ui.painter()
|
||||
.rect_filled(rect.expand2(expansion), rounding, fill);
|
||||
|
||||
let image_rect = ui
|
||||
.layout()
|
||||
.align_size_within_rect(image.size(), rect.shrink2(padding));
|
||||
.align_size_within_rect(texture.size, rect.shrink2(padding));
|
||||
// let image_rect = image_rect.expand2(expansion); // can make it blurry, so let's not
|
||||
image.paint_at(ui, image_rect);
|
||||
let image_options = ImageOptions {
|
||||
rounding,
|
||||
..Default::default()
|
||||
}; // apply rounding to the image
|
||||
crate::widgets::image::paint_image_at(ui, image_rect, &image_options, texture);
|
||||
|
||||
// Draw frame outline:
|
||||
ui.painter()
|
||||
|
|
@ -578,3 +577,17 @@ impl Widget for ImageButton {
|
|||
response
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for ImageButton<'a> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
match self.image.load(ui) {
|
||||
Ok(TexturePoll::Ready { texture }) => self.show(ui, &texture),
|
||||
Ok(TexturePoll::Pending { .. }) => ui
|
||||
.spinner()
|
||||
.on_hover_text(format!("Loading {:?}…", self.image.uri())),
|
||||
Err(err) => ui
|
||||
.colored_label(ui.visuals().error_fg_color, "⚠")
|
||||
.on_hover_text(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,90 +1,176 @@
|
|||
use std::sync::Arc;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::load::Bytes;
|
||||
use crate::{load::SizeHint, load::TexturePoll, *};
|
||||
use crate::load::TextureLoadResult;
|
||||
use crate::{
|
||||
load::{Bytes, SizeHint, SizedTexture, TexturePoll},
|
||||
*,
|
||||
};
|
||||
use emath::Rot2;
|
||||
use epaint::{util::FloatOrd, RectShape};
|
||||
|
||||
/// An widget to show an image of a given size.
|
||||
/// A widget which displays an image.
|
||||
///
|
||||
/// In order to display an image you must first acquire a [`TextureHandle`].
|
||||
/// This is best done with [`egui_extras::RetainedImage`](https://docs.rs/egui_extras/latest/egui_extras/image/struct.RetainedImage.html) or [`Context::load_texture`].
|
||||
/// The task of actually loading the image is deferred to when the `Image` is added to the [`Ui`],
|
||||
/// and how it is loaded depends on the provided [`ImageSource`]:
|
||||
///
|
||||
/// ```
|
||||
/// struct MyImage {
|
||||
/// texture: Option<egui::TextureHandle>,
|
||||
/// }
|
||||
/// - [`ImageSource::Uri`] will load the image using the [asynchronous loading process][`load`].
|
||||
/// - [`ImageSource::Bytes`] will also load the image using the [asynchronous loading process][`load`], but with lower latency.
|
||||
/// - [`ImageSource::Texture`] will use the provided texture.
|
||||
///
|
||||
/// impl MyImage {
|
||||
/// fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
/// let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
|
||||
/// // Load the texture only once.
|
||||
/// ui.ctx().load_texture(
|
||||
/// "my-image",
|
||||
/// egui::ColorImage::example(),
|
||||
/// Default::default()
|
||||
/// )
|
||||
/// });
|
||||
/// To use a texture you already have with a simpler API, consider using [`RawImage`].
|
||||
///
|
||||
/// // Show the image:
|
||||
/// ui.add(egui::Image::new(texture, texture.size_vec2()));
|
||||
///
|
||||
/// // Shorter version:
|
||||
/// ui.image(texture, texture.size_vec2());
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Se also [`crate::Ui::image`] and [`crate::ImageButton`].
|
||||
/// See [`load`] for more information.
|
||||
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Image {
|
||||
texture_id: TextureId,
|
||||
uv: Rect,
|
||||
size: Vec2,
|
||||
bg_fill: Color32,
|
||||
tint: Color32,
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Image<'a> {
|
||||
source: ImageSource<'a>,
|
||||
texture_options: TextureOptions,
|
||||
image_options: ImageOptions,
|
||||
sense: Sense,
|
||||
rotation: Option<(Rot2, Vec2)>,
|
||||
rounding: Rounding,
|
||||
size: ImageSize,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
pub fn new(texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Self {
|
||||
impl<'a> Image<'a> {
|
||||
/// Load the image from some source.
|
||||
pub fn new(source: ImageSource<'a>) -> Self {
|
||||
Self {
|
||||
texture_id: texture_id.into(),
|
||||
uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
|
||||
size: size.into(),
|
||||
bg_fill: Default::default(),
|
||||
tint: Color32::WHITE,
|
||||
source,
|
||||
texture_options: Default::default(),
|
||||
image_options: Default::default(),
|
||||
sense: Sense::hover(),
|
||||
rotation: None,
|
||||
rounding: Rounding::ZERO,
|
||||
size: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the image from a URI.
|
||||
///
|
||||
/// See [`ImageSource::Uri`].
|
||||
pub fn from_uri(uri: impl Into<Cow<'a, str>>) -> Self {
|
||||
Self::new(ImageSource::Uri(uri.into()))
|
||||
}
|
||||
|
||||
/// Load the image from an existing texture.
|
||||
///
|
||||
/// See [`ImageSource::Texture`].
|
||||
pub fn from_texture(texture: SizedTexture) -> Self {
|
||||
Self::new(ImageSource::Texture(texture))
|
||||
}
|
||||
|
||||
/// Load the image from some raw bytes.
|
||||
///
|
||||
/// See [`ImageSource::Bytes`].
|
||||
pub fn from_bytes(uri: &'static str, bytes: impl Into<Bytes>) -> Self {
|
||||
Self::new(ImageSource::Bytes(uri, bytes.into()))
|
||||
}
|
||||
|
||||
/// Texture options used when creating the texture.
|
||||
#[inline]
|
||||
pub fn texture_options(mut self, texture_options: TextureOptions) -> Self {
|
||||
self.texture_options = texture_options;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the max width of the image.
|
||||
///
|
||||
/// No matter what the image is scaled to, it will never exceed this limit.
|
||||
#[inline]
|
||||
pub fn max_width(mut self, width: f32) -> Self {
|
||||
match self.size.max_size.as_mut() {
|
||||
Some(max_size) => max_size.x = width,
|
||||
None => self.size.max_size = Some(Vec2::new(width, f32::INFINITY)),
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the max height of the image.
|
||||
///
|
||||
/// No matter what the image is scaled to, it will never exceed this limit.
|
||||
#[inline]
|
||||
pub fn max_height(mut self, height: f32) -> Self {
|
||||
match self.size.max_size.as_mut() {
|
||||
Some(max_size) => max_size.y = height,
|
||||
None => self.size.max_size = Some(Vec2::new(f32::INFINITY, height)),
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the max size of the image.
|
||||
///
|
||||
/// No matter what the image is scaled to, it will never exceed this limit.
|
||||
#[inline]
|
||||
pub fn max_size(mut self, size: Option<Vec2>) -> Self {
|
||||
self.size.max_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether or not the [`ImageFit`] should maintain the image's original aspect ratio.
|
||||
#[inline]
|
||||
pub fn maintain_aspect_ratio(mut self, value: bool) -> Self {
|
||||
self.size.maintain_aspect_ratio = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Fit the image to its original size.
|
||||
///
|
||||
/// This will cause the image to overflow if it is larger than the available space.
|
||||
///
|
||||
/// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit.
|
||||
#[inline]
|
||||
pub fn fit_to_original_size(mut self, scale: Option<f32>) -> Self {
|
||||
self.size.fit = ImageFit::Original(scale);
|
||||
self
|
||||
}
|
||||
|
||||
/// Fit the image to an exact size.
|
||||
///
|
||||
/// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit.
|
||||
#[inline]
|
||||
pub fn fit_to_exact_size(mut self, size: Vec2) -> Self {
|
||||
self.size.fit = ImageFit::Exact(size);
|
||||
self
|
||||
}
|
||||
|
||||
/// Fit the image to a fraction of the available space.
|
||||
///
|
||||
/// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit.
|
||||
#[inline]
|
||||
pub fn fit_to_fraction(mut self, fraction: Vec2) -> Self {
|
||||
self.size.fit = ImageFit::Fraction(fraction);
|
||||
self
|
||||
}
|
||||
|
||||
/// Fit the image to 100% of its available size, shrinking it if necessary.
|
||||
///
|
||||
/// This is a shorthand for [`Image::fit_to_fraction`] with `1.0` for both width and height.
|
||||
///
|
||||
/// If [`Image::max_size`] is set, this is guaranteed to never exceed that limit.
|
||||
#[inline]
|
||||
pub fn shrink_to_fit(self) -> Self {
|
||||
self.fit_to_fraction(Vec2::new(1.0, 1.0))
|
||||
}
|
||||
|
||||
/// Make the image respond to clicks and/or drags.
|
||||
#[inline]
|
||||
pub fn sense(mut self, sense: Sense) -> Self {
|
||||
self.sense = sense;
|
||||
self
|
||||
}
|
||||
|
||||
/// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
|
||||
pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
|
||||
self.uv = uv.into();
|
||||
self.image_options.uv = uv.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// A solid color to put behind the image. Useful for transparent images.
|
||||
pub fn bg_fill(mut self, bg_fill: impl Into<Color32>) -> Self {
|
||||
self.bg_fill = bg_fill.into();
|
||||
self.image_options.bg_fill = bg_fill.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Multiply image color with this. Default is WHITE (no tint).
|
||||
pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
|
||||
self.tint = tint.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Make the image respond to clicks and/or drags.
|
||||
///
|
||||
/// Consider using [`ImageButton`] instead, for an on-hover effect.
|
||||
pub fn sense(mut self, sense: Sense) -> Self {
|
||||
self.sense = sense;
|
||||
self.image_options.tint = tint.into();
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -98,8 +184,8 @@ impl Image {
|
|||
/// Due to limitations in the current implementation,
|
||||
/// this will turn off rounding of the image.
|
||||
pub fn rotate(mut self, angle: f32, origin: Vec2) -> Self {
|
||||
self.rotation = Some((Rot2::from_angle(angle), origin));
|
||||
self.rounding = Rounding::ZERO; // incompatible with rotation
|
||||
self.image_options.rotation = Some((Rot2::from_angle(angle), origin));
|
||||
self.image_options.rounding = Rounding::ZERO; // incompatible with rotation
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -110,144 +196,265 @@ impl Image {
|
|||
/// Due to limitations in the current implementation,
|
||||
/// this will turn off any rotation of the image.
|
||||
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
|
||||
self.rounding = rounding.into();
|
||||
if self.rounding != Rounding::ZERO {
|
||||
self.rotation = None; // incompatible with rounding
|
||||
self.image_options.rounding = rounding.into();
|
||||
if self.image_options.rounding != Rounding::ZERO {
|
||||
self.image_options.rotation = None; // incompatible with rounding
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Image {
|
||||
pub fn size(&self) -> Vec2 {
|
||||
self.size
|
||||
impl<'a> Image<'a> {
|
||||
/// Returns the size the image will occupy in the final UI.
|
||||
pub fn calculate_size(&self, available_size: Vec2, image_size: Vec2) -> Vec2 {
|
||||
self.size.get(available_size, image_size)
|
||||
}
|
||||
|
||||
pub fn paint_at(&self, ui: &mut Ui, rect: Rect) {
|
||||
if ui.is_rect_visible(rect) {
|
||||
use epaint::*;
|
||||
let Self {
|
||||
texture_id,
|
||||
uv,
|
||||
size,
|
||||
bg_fill,
|
||||
tint,
|
||||
sense: _,
|
||||
rotation,
|
||||
rounding,
|
||||
} = self;
|
||||
pub fn size(&self) -> Option<Vec2> {
|
||||
match &self.source {
|
||||
ImageSource::Texture(texture) => Some(texture.size),
|
||||
ImageSource::Uri(_) | ImageSource::Bytes(_, _) => None,
|
||||
}
|
||||
}
|
||||
|
||||
if *bg_fill != Default::default() {
|
||||
let mut mesh = Mesh::default();
|
||||
mesh.add_colored_rect(rect, *bg_fill);
|
||||
ui.painter().add(Shape::mesh(mesh));
|
||||
}
|
||||
pub fn source(&self) -> &ImageSource<'a> {
|
||||
&self.source
|
||||
}
|
||||
|
||||
if let Some((rot, origin)) = rotation {
|
||||
// TODO(emilk): implement this using `PathShape` (add texture support to it).
|
||||
// This will also give us anti-aliasing of rotated images.
|
||||
egui_assert!(
|
||||
*rounding == Rounding::ZERO,
|
||||
"Image had both rounding and rotation. Please pick only one"
|
||||
);
|
||||
/// Get the `uri` that this image was constructed from.
|
||||
///
|
||||
/// This will return `<unknown>` for [`ImageSource::Texture`].
|
||||
pub fn uri(&self) -> &str {
|
||||
match &self.source {
|
||||
ImageSource::Bytes(uri, _) => uri,
|
||||
ImageSource::Uri(uri) => uri,
|
||||
// Note: texture source is never in "loading" state
|
||||
ImageSource::Texture(_) => "<unknown>",
|
||||
}
|
||||
}
|
||||
|
||||
let mut mesh = Mesh::with_texture(*texture_id);
|
||||
mesh.add_rect_with_uv(rect, *uv, *tint);
|
||||
mesh.rotate(*rot, rect.min + *origin * *size);
|
||||
ui.painter().add(Shape::mesh(mesh));
|
||||
} else {
|
||||
ui.painter().add(RectShape {
|
||||
rect,
|
||||
rounding: *rounding,
|
||||
fill: *tint,
|
||||
stroke: Stroke::NONE,
|
||||
fill_texture_id: *texture_id,
|
||||
uv: *uv,
|
||||
});
|
||||
/// Load the image from its [`Image::source`], returning the resulting [`SizedTexture`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// May fail if they underlying [`Context::try_load_texture`] call fails.
|
||||
pub fn load(&self, ui: &Ui) -> TextureLoadResult {
|
||||
match self.source.clone() {
|
||||
ImageSource::Texture(texture) => Ok(TexturePoll::Ready { texture }),
|
||||
ImageSource::Uri(uri) => ui.ctx().try_load_texture(
|
||||
uri.as_ref(),
|
||||
self.texture_options,
|
||||
self.size.hint(ui.available_size()),
|
||||
),
|
||||
ImageSource::Bytes(uri, bytes) => {
|
||||
ui.ctx().include_bytes(uri.as_ref(), bytes);
|
||||
ui.ctx().try_load_texture(
|
||||
uri.as_ref(),
|
||||
self.texture_options,
|
||||
self.size.hint(ui.available_size()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paint_at(&self, ui: &mut Ui, rect: Rect, texture: &SizedTexture) {
|
||||
paint_image_at(ui, rect, &self.image_options, texture);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Image {
|
||||
impl<'a> Widget for Image<'a> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let (rect, response) = ui.allocate_exact_size(self.size, self.sense);
|
||||
self.paint_at(ui, rect);
|
||||
response
|
||||
match self.load(ui) {
|
||||
Ok(TexturePoll::Ready { texture }) => {
|
||||
let size = self.calculate_size(ui.available_size(), texture.size);
|
||||
let (rect, response) = ui.allocate_exact_size(size, self.sense);
|
||||
self.paint_at(ui, rect, &texture);
|
||||
response
|
||||
}
|
||||
Ok(TexturePoll::Pending { size }) => match size {
|
||||
Some(size) => {
|
||||
let size = self.calculate_size(ui.available_size(), size);
|
||||
ui.allocate_ui(size, |ui| {
|
||||
ui.with_layout(Layout::centered_and_justified(Direction::TopDown), |ui| {
|
||||
ui.spinner()
|
||||
.on_hover_text(format!("Loading {:?}…", self.uri()))
|
||||
})
|
||||
})
|
||||
.response
|
||||
}
|
||||
None => ui
|
||||
.spinner()
|
||||
.on_hover_text(format!("Loading {:?}…", self.uri())),
|
||||
},
|
||||
Err(err) => ui
|
||||
.colored_label(ui.visuals().error_fg_color, "⚠")
|
||||
.on_hover_text(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget which displays an image.
|
||||
///
|
||||
/// There are three ways to construct this widget:
|
||||
/// - [`Image2::from_uri`]
|
||||
/// - [`Image2::from_bytes`]
|
||||
/// - [`Image2::from_static_bytes`]
|
||||
///
|
||||
/// In both cases the task of actually loading the image
|
||||
/// is deferred to when the `Image2` is added to the [`Ui`].
|
||||
///
|
||||
/// See [`crate::load`] for more information.
|
||||
pub struct Image2<'a> {
|
||||
source: ImageSource<'a>,
|
||||
texture_options: TextureOptions,
|
||||
size_hint: SizeHint,
|
||||
fit: ImageFit,
|
||||
sense: Sense,
|
||||
/// This type determines the constraints on how
|
||||
/// the size of an image should be calculated.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ImageSize {
|
||||
/// Whether or not the final size should maintain the original aspect ratio.
|
||||
///
|
||||
/// This setting is applied last.
|
||||
///
|
||||
/// This defaults to `true`.
|
||||
pub maintain_aspect_ratio: bool,
|
||||
|
||||
/// Determines the maximum size of the image.
|
||||
///
|
||||
/// Defaults to `None`
|
||||
pub max_size: Option<Vec2>,
|
||||
|
||||
/// Determines how the image should shrink/expand/stretch/etc. to fit within its allocated space.
|
||||
///
|
||||
/// This setting is applied first.
|
||||
///
|
||||
/// Defaults to `ImageFit::Fraction([1, 1])`
|
||||
pub fit: ImageFit,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
enum ImageFit {
|
||||
// TODO: options for aspect ratio
|
||||
// TODO: other fit strategies
|
||||
// FitToWidth,
|
||||
// FitToHeight,
|
||||
// FitToWidthExact(f32),
|
||||
// FitToHeightExact(f32),
|
||||
#[default]
|
||||
ShrinkToFit,
|
||||
/// This type determines how the image should try to fit within the UI.
|
||||
///
|
||||
/// The final fit will be clamped to [`ImageSize::max_size`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub enum ImageFit {
|
||||
/// Fit the image to its original size, optionally scaling it by some factor.
|
||||
Original(Option<f32>),
|
||||
|
||||
/// Fit the image to a fraction of the available size.
|
||||
Fraction(Vec2),
|
||||
|
||||
/// Fit the image to an exact size.
|
||||
Exact(Vec2),
|
||||
}
|
||||
|
||||
impl ImageFit {
|
||||
pub fn calculate_final_size(&self, available_size: Vec2, image_size: Vec2) -> Vec2 {
|
||||
let aspect_ratio = image_size.x / image_size.y;
|
||||
// TODO: more image sizing options
|
||||
match self {
|
||||
// ImageFit::FitToWidth => todo!(),
|
||||
// ImageFit::FitToHeight => todo!(),
|
||||
// ImageFit::FitToWidthExact(_) => todo!(),
|
||||
// ImageFit::FitToHeightExact(_) => todo!(),
|
||||
ImageFit::ShrinkToFit => {
|
||||
let width = if available_size.x < image_size.x {
|
||||
available_size.x
|
||||
} else {
|
||||
image_size.x
|
||||
};
|
||||
let height = if available_size.y < image_size.y {
|
||||
available_size.y
|
||||
} else {
|
||||
image_size.y
|
||||
};
|
||||
if width < height {
|
||||
Vec2::new(width, width / aspect_ratio)
|
||||
} else {
|
||||
Vec2::new(height * aspect_ratio, height)
|
||||
impl ImageSize {
|
||||
fn hint(&self, available_size: Vec2) -> SizeHint {
|
||||
if self.maintain_aspect_ratio {
|
||||
return SizeHint::Scale(1.0.ord());
|
||||
};
|
||||
|
||||
let fit = match self.fit {
|
||||
ImageFit::Original(scale) => return SizeHint::Scale(scale.unwrap_or(1.0).ord()),
|
||||
ImageFit::Fraction(fract) => available_size * fract,
|
||||
ImageFit::Exact(size) => size,
|
||||
};
|
||||
|
||||
let fit = match self.max_size {
|
||||
Some(extent) => fit.min(extent),
|
||||
None => fit,
|
||||
};
|
||||
|
||||
// `inf` on an axis means "any value"
|
||||
match (fit.x.is_finite(), fit.y.is_finite()) {
|
||||
(true, true) => SizeHint::Size(fit.x.round() as u32, fit.y.round() as u32),
|
||||
(true, false) => SizeHint::Width(fit.x.round() as u32),
|
||||
(false, true) => SizeHint::Height(fit.y.round() as u32),
|
||||
(false, false) => SizeHint::Scale(1.0.ord()),
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, available_size: Vec2, image_size: Vec2) -> Vec2 {
|
||||
match self.fit {
|
||||
ImageFit::Original(scale) => {
|
||||
let image_size = image_size * scale.unwrap_or(1.0);
|
||||
|
||||
if let Some(available_size) = self.max_size {
|
||||
if image_size.x < available_size.x && image_size.y < available_size.y {
|
||||
return image_size;
|
||||
}
|
||||
|
||||
if self.maintain_aspect_ratio {
|
||||
let ratio_x = available_size.x / image_size.x;
|
||||
let ratio_y = available_size.y / image_size.y;
|
||||
let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y };
|
||||
let ratio = if ratio.is_infinite() { 1.0 } else { ratio };
|
||||
|
||||
return Vec2::new(image_size.x * ratio, image_size.y * ratio);
|
||||
} else {
|
||||
return image_size.min(available_size);
|
||||
}
|
||||
}
|
||||
|
||||
image_size
|
||||
}
|
||||
ImageFit::Fraction(fract) => {
|
||||
let available_size = available_size * fract;
|
||||
let available_size = match self.max_size {
|
||||
Some(max_size) => available_size.min(max_size),
|
||||
None => available_size,
|
||||
};
|
||||
|
||||
if self.maintain_aspect_ratio {
|
||||
let ratio_x = available_size.x / image_size.x;
|
||||
let ratio_y = available_size.y / image_size.y;
|
||||
let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y };
|
||||
let ratio = if ratio.is_infinite() { 1.0 } else { ratio };
|
||||
|
||||
return Vec2::new(image_size.x * ratio, image_size.y * ratio);
|
||||
}
|
||||
|
||||
available_size
|
||||
}
|
||||
ImageFit::Exact(size) => {
|
||||
let available_size = size;
|
||||
let available_size = match self.max_size {
|
||||
Some(max_size) => available_size.min(max_size),
|
||||
None => available_size,
|
||||
};
|
||||
|
||||
if self.maintain_aspect_ratio {
|
||||
let ratio_x = available_size.x / image_size.x;
|
||||
let ratio_y = available_size.y / image_size.y;
|
||||
let ratio = if ratio_x < ratio_y { ratio_x } else { ratio_y };
|
||||
let ratio = if ratio.is_infinite() { 1.0 } else { ratio };
|
||||
|
||||
return Vec2::new(image_size.x * ratio, image_size.y * ratio);
|
||||
}
|
||||
|
||||
available_size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This type tells the [`Ui`] how to load the image.
|
||||
impl Default for ImageSize {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_size: None,
|
||||
fit: ImageFit::Fraction(Vec2::new(1.0, 1.0)),
|
||||
maintain_aspect_ratio: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This type tells the [`Ui`] how to load an image.
|
||||
///
|
||||
/// This is used by [`Image::new`] and [`Ui::image`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ImageSource<'a> {
|
||||
/// Load the image from a URI.
|
||||
///
|
||||
/// This could be a `file://` url, `http://` url, or a `bare` identifier.
|
||||
/// This could be a `file://` url, `http(s)?://` url, or a `bare` identifier.
|
||||
/// How the URI will be turned into a texture for rendering purposes is
|
||||
/// up to the registered loaders to handle.
|
||||
///
|
||||
/// See [`crate::load`] for more information.
|
||||
Uri(&'a str),
|
||||
Uri(Cow<'a, str>),
|
||||
|
||||
/// Load the image from an existing texture.
|
||||
///
|
||||
/// The user is responsible for loading the texture, determining its size,
|
||||
/// and allocating a [`TextureId`] for it.
|
||||
///
|
||||
/// Note that a simpler API for this exists in [`RawImage`].
|
||||
Texture(SizedTexture),
|
||||
|
||||
/// Load the image from some raw bytes.
|
||||
///
|
||||
|
|
@ -257,6 +464,8 @@ pub enum ImageSource<'a> {
|
|||
///
|
||||
/// This instructs the [`Ui`] to cache the raw bytes, which are then further processed by any registered loaders.
|
||||
///
|
||||
/// See also [`include_image`] for an easy way to load and display static images.
|
||||
///
|
||||
/// See [`crate::load`] for more information.
|
||||
Bytes(&'static str, Bytes),
|
||||
}
|
||||
|
|
@ -264,6 +473,33 @@ pub enum ImageSource<'a> {
|
|||
impl<'a> From<&'a str> for ImageSource<'a> {
|
||||
#[inline]
|
||||
fn from(value: &'a str) -> Self {
|
||||
Self::Uri(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a String> for ImageSource<'a> {
|
||||
#[inline]
|
||||
fn from(value: &'a String) -> Self {
|
||||
Self::Uri(value.as_str().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ImageSource<'static> {
|
||||
fn from(value: String) -> Self {
|
||||
Self::Uri(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Cow<'a, str>> for ImageSource<'a> {
|
||||
#[inline]
|
||||
fn from(value: &'a Cow<'a, str>) -> Self {
|
||||
Self::Uri(value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, str>> for ImageSource<'a> {
|
||||
#[inline]
|
||||
fn from(value: Cow<'a, str>) -> Self {
|
||||
Self::Uri(value)
|
||||
}
|
||||
}
|
||||
|
|
@ -275,55 +511,29 @@ impl<T: Into<Bytes>> From<(&'static str, T)> for ImageSource<'static> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> Image2<'a> {
|
||||
impl<T: Into<SizedTexture>> From<T> for ImageSource<'static> {
|
||||
fn from(value: T) -> Self {
|
||||
Self::Texture(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget which displays a sized texture.
|
||||
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RawImage {
|
||||
texture: SizedTexture,
|
||||
texture_options: TextureOptions,
|
||||
image_options: ImageOptions,
|
||||
sense: Sense,
|
||||
}
|
||||
|
||||
impl RawImage {
|
||||
/// Load the image from some source.
|
||||
pub fn new(source: ImageSource<'a>) -> Self {
|
||||
pub fn new(texture: impl Into<SizedTexture>) -> Self {
|
||||
Self {
|
||||
source,
|
||||
texture: texture.into(),
|
||||
texture_options: Default::default(),
|
||||
size_hint: Default::default(),
|
||||
fit: Default::default(),
|
||||
sense: Sense::hover(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the image from a URI.
|
||||
///
|
||||
/// See [`ImageSource::Uri`].
|
||||
pub fn from_uri(uri: &'a str) -> Self {
|
||||
Self {
|
||||
source: ImageSource::Uri(uri),
|
||||
texture_options: Default::default(),
|
||||
size_hint: Default::default(),
|
||||
fit: Default::default(),
|
||||
sense: Sense::hover(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the image from some raw `'static` bytes.
|
||||
///
|
||||
/// For example, you can use this to load an image from bytes obtained via [`include_bytes`].
|
||||
///
|
||||
/// See [`ImageSource::Bytes`].
|
||||
pub fn from_static_bytes(name: &'static str, bytes: &'static [u8]) -> Self {
|
||||
Self {
|
||||
source: ImageSource::Bytes(name, Bytes::Static(bytes)),
|
||||
texture_options: Default::default(),
|
||||
size_hint: Default::default(),
|
||||
fit: Default::default(),
|
||||
sense: Sense::hover(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the image from some raw bytes.
|
||||
///
|
||||
/// See [`ImageSource::Bytes`].
|
||||
pub fn from_bytes(name: &'static str, bytes: impl Into<Arc<[u8]>>) -> Self {
|
||||
Self {
|
||||
source: ImageSource::Bytes(name, Bytes::Shared(bytes.into())),
|
||||
texture_options: Default::default(),
|
||||
size_hint: Default::default(),
|
||||
fit: Default::default(),
|
||||
image_options: Default::default(),
|
||||
sense: Sense::hover(),
|
||||
}
|
||||
}
|
||||
|
|
@ -335,60 +545,164 @@ impl<'a> Image2<'a> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Size hint used when creating the texture.
|
||||
#[inline]
|
||||
pub fn size_hint(mut self, size_hint: impl Into<SizeHint>) -> Self {
|
||||
self.size_hint = size_hint.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Make the image respond to clicks and/or drags.
|
||||
#[inline]
|
||||
pub fn sense(mut self, sense: Sense) -> Self {
|
||||
self.sense = sense;
|
||||
self
|
||||
}
|
||||
|
||||
/// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
|
||||
pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
|
||||
self.image_options.uv = uv.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// A solid color to put behind the image. Useful for transparent images.
|
||||
pub fn bg_fill(mut self, bg_fill: impl Into<Color32>) -> Self {
|
||||
self.image_options.bg_fill = bg_fill.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Multiply image color with this. Default is WHITE (no tint).
|
||||
pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
|
||||
self.image_options.tint = tint.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Rotate the image about an origin by some angle
|
||||
///
|
||||
/// Positive angle is clockwise.
|
||||
/// Origin is a vector in normalized UV space ((0,0) in top-left, (1,1) bottom right).
|
||||
///
|
||||
/// To rotate about the center you can pass `Vec2::splat(0.5)` as the origin.
|
||||
///
|
||||
/// Due to limitations in the current implementation,
|
||||
/// this will turn off rounding of the image.
|
||||
pub fn rotate(mut self, angle: f32, origin: Vec2) -> Self {
|
||||
self.image_options.rotation = Some((Rot2::from_angle(angle), origin));
|
||||
self.image_options.rounding = Rounding::ZERO; // incompatible with rotation
|
||||
self
|
||||
}
|
||||
|
||||
/// Round the corners of the image.
|
||||
///
|
||||
/// The default is no rounding ([`Rounding::ZERO`]).
|
||||
///
|
||||
/// Due to limitations in the current implementation,
|
||||
/// this will turn off any rotation of the image.
|
||||
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
|
||||
self.image_options.rounding = rounding.into();
|
||||
if self.image_options.rounding != Rounding::ZERO {
|
||||
self.image_options.rotation = None; // incompatible with rounding
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Image2<'a> {
|
||||
impl RawImage {
|
||||
/// Returns the [`TextureId`] of the texture from which this image was created.
|
||||
pub fn texture_id(&self) -> TextureId {
|
||||
self.texture.id
|
||||
}
|
||||
|
||||
/// Returns the size of the texture from which this image was created.
|
||||
pub fn size(&self) -> Vec2 {
|
||||
self.texture.size
|
||||
}
|
||||
|
||||
pub fn paint_at(&self, ui: &mut Ui, rect: Rect) {
|
||||
paint_image_at(ui, rect, &self.image_options, &self.texture);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for RawImage {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let uri = match self.source {
|
||||
ImageSource::Uri(uri) => uri,
|
||||
ImageSource::Bytes(uri, bytes) => {
|
||||
match bytes {
|
||||
Bytes::Static(bytes) => ui.ctx().include_static_bytes(uri, bytes),
|
||||
Bytes::Shared(bytes) => ui.ctx().include_bytes(uri, bytes),
|
||||
}
|
||||
uri
|
||||
}
|
||||
};
|
||||
let (rect, response) = ui.allocate_exact_size(self.size(), self.sense);
|
||||
self.paint_at(ui, rect);
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
match ui
|
||||
.ctx()
|
||||
.try_load_texture(uri, self.texture_options, self.size_hint)
|
||||
{
|
||||
Ok(TexturePoll::Ready { texture }) => {
|
||||
let final_size = self.fit.calculate_final_size(
|
||||
ui.available_size(),
|
||||
Vec2::new(texture.size[0] as f32, texture.size[1] as f32),
|
||||
);
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct ImageOptions {
|
||||
/// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
|
||||
pub uv: Rect,
|
||||
|
||||
let (rect, response) = ui.allocate_exact_size(final_size, self.sense);
|
||||
/// A solid color to put behind the image. Useful for transparent images.
|
||||
pub bg_fill: Color32,
|
||||
|
||||
let mut mesh = Mesh::with_texture(texture.id);
|
||||
mesh.add_rect_with_uv(
|
||||
rect,
|
||||
Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
|
||||
Color32::WHITE,
|
||||
);
|
||||
ui.painter().add(Shape::mesh(mesh));
|
||||
/// Multiply image color with this. Default is WHITE (no tint).
|
||||
pub tint: Color32,
|
||||
|
||||
response
|
||||
}
|
||||
Ok(TexturePoll::Pending { .. }) => {
|
||||
ui.spinner().on_hover_text(format!("Loading {uri:?}…"))
|
||||
}
|
||||
Err(err) => ui.colored_label(ui.visuals().error_fg_color, err.to_string()),
|
||||
/// Rotate the image about an origin by some angle
|
||||
///
|
||||
/// Positive angle is clockwise.
|
||||
/// Origin is a vector in normalized UV space ((0,0) in top-left, (1,1) bottom right).
|
||||
///
|
||||
/// To rotate about the center you can pass `Vec2::splat(0.5)` as the origin.
|
||||
///
|
||||
/// Due to limitations in the current implementation,
|
||||
/// this will turn off rounding of the image.
|
||||
pub rotation: Option<(Rot2, Vec2)>,
|
||||
|
||||
/// Round the corners of the image.
|
||||
///
|
||||
/// The default is no rounding ([`Rounding::ZERO`]).
|
||||
///
|
||||
/// Due to limitations in the current implementation,
|
||||
/// this will turn off any rotation of the image.
|
||||
pub rounding: Rounding,
|
||||
}
|
||||
|
||||
impl Default for ImageOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
|
||||
bg_fill: Default::default(),
|
||||
tint: Color32::WHITE,
|
||||
rotation: None,
|
||||
rounding: Rounding::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint a `SizedTexture` as an image according to some `ImageOptions` at a given `rect`.
|
||||
pub fn paint_image_at(ui: &mut Ui, rect: Rect, options: &ImageOptions, texture: &SizedTexture) {
|
||||
if !ui.is_rect_visible(rect) {
|
||||
return;
|
||||
}
|
||||
|
||||
if options.bg_fill != Default::default() {
|
||||
let mut mesh = Mesh::default();
|
||||
mesh.add_colored_rect(rect, options.bg_fill);
|
||||
ui.painter().add(Shape::mesh(mesh));
|
||||
}
|
||||
|
||||
match options.rotation {
|
||||
Some((rot, origin)) => {
|
||||
// TODO(emilk): implement this using `PathShape` (add texture support to it).
|
||||
// This will also give us anti-aliasing of rotated images.
|
||||
egui_assert!(
|
||||
options.rounding == Rounding::ZERO,
|
||||
"Image had both rounding and rotation. Please pick only one"
|
||||
);
|
||||
|
||||
let mut mesh = Mesh::with_texture(texture.id);
|
||||
mesh.add_rect_with_uv(rect, options.uv, options.tint);
|
||||
mesh.rotate(rot, rect.min + origin * rect.size());
|
||||
ui.painter().add(Shape::mesh(mesh));
|
||||
}
|
||||
None => {
|
||||
ui.painter().add(RectShape {
|
||||
rect,
|
||||
rounding: options.rounding,
|
||||
fill: options.tint,
|
||||
stroke: Stroke::NONE,
|
||||
fill_texture_id: texture.id,
|
||||
uv: options.uv,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ pub mod text_edit;
|
|||
pub use button::*;
|
||||
pub use drag_value::DragValue;
|
||||
pub use hyperlink::*;
|
||||
pub use image::{Image, Image2, ImageSource};
|
||||
pub use image::{Image, ImageFit, ImageOptions, ImageSize, ImageSource, RawImage};
|
||||
pub use label::*;
|
||||
pub use progress_bar::ProgressBar;
|
||||
pub use selected_label::SelectableLabel;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ crate-type = ["cdylib", "rlib"]
|
|||
default = ["glow", "persistence"]
|
||||
|
||||
http = ["ehttp", "image", "poll-promise", "egui_extras/image"]
|
||||
image_viewer = ["image", "egui_extras/all-loaders", "rfd"]
|
||||
persistence = ["eframe/persistence", "egui/persistence", "serde"]
|
||||
web_screen_reader = ["eframe/web_screen_reader"] # experimental
|
||||
serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]
|
||||
|
|
@ -27,7 +28,6 @@ syntax_highlighting = ["egui_demo_lib/syntax_highlighting"]
|
|||
glow = ["eframe/glow"]
|
||||
wgpu = ["eframe/wgpu", "bytemuck"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4", default-features = false, features = [
|
||||
"js-sys",
|
||||
|
|
@ -48,6 +48,7 @@ bytemuck = { version = "1.7.1", optional = true }
|
|||
egui_extras = { version = "0.22.0", optional = true, path = "../egui_extras", features = [
|
||||
"log",
|
||||
] }
|
||||
rfd = { version = "0.11", optional = true }
|
||||
|
||||
# feature "http":
|
||||
ehttp = { version = "0.3.0", optional = true }
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(deprecated)]
|
||||
|
||||
use egui_extras::RetainedImage;
|
||||
use poll_promise::Promise;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,217 @@
|
|||
use egui::emath::Rot2;
|
||||
use egui::panel::Side;
|
||||
use egui::panel::TopBottomSide;
|
||||
use egui::ImageFit;
|
||||
use egui::Slider;
|
||||
use egui::Vec2;
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct ImageViewer {
|
||||
current_uri: String,
|
||||
uri_edit_text: String,
|
||||
image_options: egui::ImageOptions,
|
||||
chosen_fit: ChosenFit,
|
||||
fit: ImageFit,
|
||||
maintain_aspect_ratio: bool,
|
||||
max_size: Option<Vec2>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
enum ChosenFit {
|
||||
ExactSize,
|
||||
Fraction,
|
||||
OriginalSize,
|
||||
}
|
||||
|
||||
impl ChosenFit {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ChosenFit::ExactSize => "exact size",
|
||||
ChosenFit::Fraction => "fraction",
|
||||
ChosenFit::OriginalSize => "original size",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ImageViewer {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
current_uri: "https://picsum.photos/seed/1.759706314/1024".to_owned(),
|
||||
uri_edit_text: "https://picsum.photos/seed/1.759706314/1024".to_owned(),
|
||||
image_options: egui::ImageOptions::default(),
|
||||
chosen_fit: ChosenFit::Fraction,
|
||||
fit: ImageFit::Fraction(Vec2::splat(1.0)),
|
||||
maintain_aspect_ratio: true,
|
||||
max_size: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for ImageViewer {
|
||||
fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
|
||||
egui::TopBottomPanel::new(TopBottomSide::Top, "url bar").show(ctx, |ui| {
|
||||
ui.horizontal_centered(|ui| {
|
||||
ui.label("URI:");
|
||||
ui.text_edit_singleline(&mut self.uri_edit_text);
|
||||
if ui.small_button("✔").clicked() {
|
||||
ctx.forget_image(&self.current_uri);
|
||||
self.uri_edit_text = self.uri_edit_text.trim().to_owned();
|
||||
self.current_uri = self.uri_edit_text.clone();
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if ui.button("file…").clicked() {
|
||||
if let Some(path) = rfd::FileDialog::new().pick_file() {
|
||||
self.uri_edit_text = format!("file://{}", path.display());
|
||||
self.current_uri = self.uri_edit_text.clone();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
egui::SidePanel::new(Side::Left, "controls").show(ctx, |ui| {
|
||||
// uv
|
||||
ui.label("UV");
|
||||
ui.add(Slider::new(&mut self.image_options.uv.min.x, 0.0..=1.0).text("min x"));
|
||||
ui.add(Slider::new(&mut self.image_options.uv.min.y, 0.0..=1.0).text("min y"));
|
||||
ui.add(Slider::new(&mut self.image_options.uv.max.x, 0.0..=1.0).text("max x"));
|
||||
ui.add(Slider::new(&mut self.image_options.uv.max.y, 0.0..=1.0).text("max y"));
|
||||
|
||||
// rotation
|
||||
ui.add_space(2.0);
|
||||
let had_rotation = self.image_options.rotation.is_some();
|
||||
let mut has_rotation = had_rotation;
|
||||
ui.checkbox(&mut has_rotation, "Rotation");
|
||||
match (had_rotation, has_rotation) {
|
||||
(true, false) => self.image_options.rotation = None,
|
||||
(false, true) => {
|
||||
self.image_options.rotation =
|
||||
Some((Rot2::from_angle(0.0), Vec2::new(0.5, 0.5)));
|
||||
}
|
||||
(true, true) | (false, false) => {}
|
||||
}
|
||||
|
||||
if let Some((rot, origin)) = self.image_options.rotation.as_mut() {
|
||||
let mut angle = rot.angle();
|
||||
|
||||
ui.label("angle");
|
||||
ui.drag_angle(&mut angle);
|
||||
*rot = Rot2::from_angle(angle);
|
||||
|
||||
ui.add(Slider::new(&mut origin.x, 0.0..=1.0).text("origin x"));
|
||||
ui.add(Slider::new(&mut origin.y, 0.0..=1.0).text("origin y"));
|
||||
}
|
||||
|
||||
// bg_fill
|
||||
ui.add_space(2.0);
|
||||
ui.label("Background color");
|
||||
ui.color_edit_button_srgba(&mut self.image_options.bg_fill);
|
||||
|
||||
// tint
|
||||
ui.add_space(2.0);
|
||||
ui.label("Tint");
|
||||
ui.color_edit_button_srgba(&mut self.image_options.tint);
|
||||
|
||||
// fit
|
||||
ui.add_space(10.0);
|
||||
ui.label(
|
||||
"The chosen fit will determine how the image tries to fill the available space",
|
||||
);
|
||||
egui::ComboBox::from_label("Fit")
|
||||
.selected_text(self.chosen_fit.as_str())
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(
|
||||
&mut self.chosen_fit,
|
||||
ChosenFit::ExactSize,
|
||||
ChosenFit::ExactSize.as_str(),
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut self.chosen_fit,
|
||||
ChosenFit::Fraction,
|
||||
ChosenFit::Fraction.as_str(),
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut self.chosen_fit,
|
||||
ChosenFit::OriginalSize,
|
||||
ChosenFit::OriginalSize.as_str(),
|
||||
);
|
||||
});
|
||||
|
||||
match self.chosen_fit {
|
||||
ChosenFit::ExactSize => {
|
||||
if !matches!(self.fit, ImageFit::Exact(_)) {
|
||||
self.fit = ImageFit::Exact(Vec2::splat(128.0));
|
||||
}
|
||||
let ImageFit::Exact(size) = &mut self.fit else { unreachable!() };
|
||||
ui.add(Slider::new(&mut size.x, 0.0..=2048.0).text("width"));
|
||||
ui.add(Slider::new(&mut size.y, 0.0..=2048.0).text("height"));
|
||||
}
|
||||
ChosenFit::Fraction => {
|
||||
if !matches!(self.fit, ImageFit::Fraction(_)) {
|
||||
self.fit = ImageFit::Fraction(Vec2::splat(1.0));
|
||||
}
|
||||
let ImageFit::Fraction(fract) = &mut self.fit else { unreachable!() };
|
||||
ui.add(Slider::new(&mut fract.x, 0.0..=1.0).text("width"));
|
||||
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));
|
||||
}
|
||||
let ImageFit::Original(Some(scale)) = &mut self.fit else { unreachable!() };
|
||||
ui.add(Slider::new(scale, 0.1..=4.0).text("scale"));
|
||||
}
|
||||
}
|
||||
|
||||
// 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"));
|
||||
}
|
||||
|
||||
// aspect ratio
|
||||
ui.add_space(5.0);
|
||||
ui.label("Aspect ratio is maintained by scaling both sides as necessary");
|
||||
ui.checkbox(&mut self.maintain_aspect_ratio, "Maintain aspect ratio");
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::ScrollArea::new([true, true]).show(ui, |ui| {
|
||||
let mut image = egui::Image::from_uri(&self.current_uri);
|
||||
image = image.uv(self.image_options.uv);
|
||||
image = image.bg_fill(self.image_options.bg_fill);
|
||||
image = image.tint(self.image_options.tint);
|
||||
let (angle, origin) = self
|
||||
.image_options
|
||||
.rotation
|
||||
.map_or((0.0, Vec2::splat(0.5)), |(rot, origin)| {
|
||||
(rot.angle(), origin)
|
||||
});
|
||||
image = image.rotate(angle, origin);
|
||||
match self.fit {
|
||||
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),
|
||||
}
|
||||
image = image.maintain_aspect_ratio(self.maintain_aspect_ratio);
|
||||
image = image.max_size(self.max_size);
|
||||
|
||||
ui.add_sized(ui.available_size(), image);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,12 @@ mod fractal_clock;
|
|||
#[cfg(feature = "http")]
|
||||
mod http_app;
|
||||
|
||||
#[cfg(feature = "image_viewer")]
|
||||
mod image_viewer;
|
||||
|
||||
#[cfg(feature = "image_viewer")]
|
||||
pub use image_viewer::ImageViewer;
|
||||
|
||||
#[cfg(all(feature = "glow", not(feature = "wgpu")))]
|
||||
pub use custom3d_glow::Custom3d;
|
||||
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ enum Anchor {
|
|||
EasyMarkEditor,
|
||||
#[cfg(feature = "http")]
|
||||
Http,
|
||||
#[cfg(feature = "image_viewer")]
|
||||
ImageViewer,
|
||||
Clock,
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
Custom3d,
|
||||
|
|
@ -142,6 +144,8 @@ pub struct State {
|
|||
easy_mark_editor: EasyMarkApp,
|
||||
#[cfg(feature = "http")]
|
||||
http: crate::apps::HttpApp,
|
||||
#[cfg(feature = "image_viewer")]
|
||||
image_viewer: crate::apps::ImageViewer,
|
||||
clock: FractalClockApp,
|
||||
color_test: ColorTestApp,
|
||||
|
||||
|
|
@ -161,6 +165,9 @@ 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)]
|
||||
let mut slf = Self {
|
||||
state: State::default(),
|
||||
|
|
@ -204,6 +211,12 @@ impl WrapApp {
|
|||
Anchor::Clock,
|
||||
&mut self.state.clock as &mut dyn eframe::App,
|
||||
),
|
||||
#[cfg(feature = "image_viewer")]
|
||||
(
|
||||
"🖼️ Image Viewer",
|
||||
Anchor::ImageViewer,
|
||||
&mut self.state.image_viewer as &mut dyn eframe::App,
|
||||
),
|
||||
];
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
|
|
|
|||
|
|
@ -87,8 +87,12 @@ impl ColorTest {
|
|||
let tex = self.tex_mngr.get(ui.ctx(), &g);
|
||||
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(Image::new(tex, GRADIENT_SIZE).tint(vertex_color).uv(uv))
|
||||
.on_hover_text(format!("A texture that is {} texels wide", g.0.len()));
|
||||
ui.add(
|
||||
RawImage::new((tex.id(), GRADIENT_SIZE))
|
||||
.tint(vertex_color)
|
||||
.uv(uv),
|
||||
)
|
||||
.on_hover_text(format!("A texture that is {} texels wide", g.0.len()));
|
||||
ui.label("GPU result");
|
||||
});
|
||||
});
|
||||
|
|
@ -225,11 +229,15 @@ impl ColorTest {
|
|||
let tex = self.tex_mngr.get(ui.ctx(), gradient);
|
||||
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(Image::new(tex, GRADIENT_SIZE).bg_fill(bg_fill).uv(uv))
|
||||
.on_hover_text(format!(
|
||||
"A texture that is {} texels wide",
|
||||
gradient.0.len()
|
||||
));
|
||||
ui.add(
|
||||
RawImage::new((tex.id(), GRADIENT_SIZE))
|
||||
.bg_fill(bg_fill)
|
||||
.uv(uv),
|
||||
)
|
||||
.on_hover_text(format!(
|
||||
"A texture that is {} texels wide",
|
||||
gradient.0.len()
|
||||
));
|
||||
ui.label(label);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -209,11 +209,14 @@ impl WidgetGallery {
|
|||
let img_size = 16.0 * texture.size_vec2() / texture.size_vec2().y;
|
||||
|
||||
ui.add(doc_link_label("Image", "Image"));
|
||||
ui.image(texture, img_size);
|
||||
ui.raw_image((texture.id(), img_size));
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("ImageButton", "ImageButton"));
|
||||
if ui.add(egui::ImageButton::new(texture, img_size)).clicked() {
|
||||
if ui
|
||||
.add(egui::ImageButton::new((texture.id(), img_size)))
|
||||
.clicked()
|
||||
{
|
||||
*boolean = !*boolean;
|
||||
}
|
||||
ui.end_row();
|
||||
|
|
|
|||
|
|
@ -24,14 +24,17 @@ all-features = true
|
|||
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["dep:mime_guess"]
|
||||
|
||||
## Shorthand for enabling `svg`, `image`, and `ehttp`.
|
||||
all-loaders = ["svg", "image", "http"]
|
||||
## Shorthand for enabling the different types of image loaders (`file`, `http`, `image`, `svg`).
|
||||
all-loaders = ["svg", "image", "http", "file"]
|
||||
|
||||
## Enable [`DatePickerButton`] widget.
|
||||
datepicker = ["chrono"]
|
||||
|
||||
## Add support for loading images from `file://` URIs.
|
||||
file = ["dep:mime_guess"]
|
||||
|
||||
## Log warnings using [`log`](https://docs.rs/log) crate.
|
||||
log = ["dep:log", "egui/log"]
|
||||
|
||||
|
|
@ -75,6 +78,9 @@ image = { version = "0.24", optional = true, default-features = false }
|
|||
# feature "log"
|
||||
log = { version = "0.4", optional = true, features = ["std"] }
|
||||
|
||||
# file feature
|
||||
mime_guess = { version = "2.0.4", optional = true, default-features = false }
|
||||
|
||||
puffin = { version = "0.16", optional = true }
|
||||
|
||||
# svg feature
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(deprecated)]
|
||||
|
||||
use egui::{mutex::Mutex, TextureFilter, TextureOptions};
|
||||
|
||||
#[cfg(feature = "svg")]
|
||||
|
|
@ -8,6 +10,9 @@ pub use usvg::FitTo;
|
|||
/// Load once, and save somewhere in your app state.
|
||||
///
|
||||
/// Use the `svg` and `image` features to enable more constructors.
|
||||
///
|
||||
/// ⚠ This type is deprecated: Consider using [`egui::Image`] instead.
|
||||
#[deprecated = "consider using `egui::Image` instead"]
|
||||
pub struct RetainedImage {
|
||||
debug_name: String,
|
||||
|
||||
|
|
@ -186,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.image(self.texture_id(ui.ctx()), desired_size)
|
||||
ui.raw_image((self.texture_id(ui.ctx()), desired_size))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ mod table;
|
|||
#[cfg(feature = "chrono")]
|
||||
pub use crate::datepicker::DatePickerButton;
|
||||
|
||||
#[allow(deprecated)]
|
||||
pub use crate::image::RetainedImage;
|
||||
pub(crate) use crate::layout::StripLayout;
|
||||
pub use crate::sizing::Size;
|
||||
|
|
@ -63,6 +64,7 @@ pub(crate) use profiling_scopes::*;
|
|||
|
||||
/// Log an error with either `log` or `eprintln`
|
||||
macro_rules! log_err {
|
||||
($fmt: literal) => {$crate::log_err!($fmt,)};
|
||||
($fmt: literal, $($arg: tt)*) => {{
|
||||
#[cfg(feature = "log")]
|
||||
log::error!($fmt, $($arg)*);
|
||||
|
|
@ -77,6 +79,7 @@ pub(crate) use log_err;
|
|||
|
||||
/// Panic in debug builds, log otherwise.
|
||||
macro_rules! log_or_panic {
|
||||
($fmt: literal) => {$crate::log_or_panic!($fmt,)};
|
||||
($fmt: literal, $($arg: tt)*) => {{
|
||||
if cfg!(debug_assertions) {
|
||||
panic!($fmt, $($arg)*);
|
||||
|
|
|
|||
|
|
@ -1,41 +1,87 @@
|
|||
// TODO: automatic cache eviction
|
||||
|
||||
/// Installs the default set of loaders:
|
||||
/// Installs the default set of loaders.
|
||||
///
|
||||
/// - `file` loader on non-Wasm targets
|
||||
/// - `http` loader (with the `ehttp` feature)
|
||||
/// - `http` loader (with the `http` feature)
|
||||
/// - `image` loader (with the `image` feature)
|
||||
/// - `svg` loader with the `svg` feature
|
||||
///
|
||||
/// ⚠ This will do nothing and you won't see any images unless you enable some features!
|
||||
/// If you just want to be able to load `file://` and `http://` images, enable the `all-loaders` feature.
|
||||
/// Calling this multiple times on the same [`egui::Context`] is safe.
|
||||
/// It will never install duplicate loaders.
|
||||
///
|
||||
/// ⚠ The supported set of image formats is configured by adding the [`image`](https://crates.io/crates/image)
|
||||
/// ⚠ This will do nothing and you won't see any images unless you enable some features:
|
||||
///
|
||||
/// - If you just want to be able to load `file://` and `http://` URIs, enable the `all-loaders` feature.
|
||||
/// - The supported set of image formats is configured by adding the [`image`](https://crates.io/crates/image)
|
||||
/// crate as your direct dependency, and enabling features on it:
|
||||
///
|
||||
/// ```toml,ignore
|
||||
/// image = { version = "0.24", features = ["jpeg", "png"] }
|
||||
/// ```
|
||||
///
|
||||
/// ⚠ You have to configure both the supported loaders in `egui_extras` _and_ the supported image formats
|
||||
/// in `image` to get any output!
|
||||
///
|
||||
/// ## Loader-specific information
|
||||
///
|
||||
/// ⚠ The exact way bytes, images, and textures are loaded is subject to change,
|
||||
/// but the supported protocols and file extensions are not.
|
||||
///
|
||||
/// The `file` loader is a [`BytesLoader`][`egui::load::BytesLoader`].
|
||||
/// It will attempt to load `file://` URIs, and infer the content type from the extension.
|
||||
/// The path will be passed to [`std::fs::read`] after trimming the `file://` prefix,
|
||||
/// and is resolved the same way as with `std::fs::read(path)`:
|
||||
/// - Relative paths are relative to the current working directory
|
||||
/// - Absolute paths are left as is.
|
||||
///
|
||||
/// The `http` loader is a [`BytesLoader`][`egui::load::BytesLoader`].
|
||||
/// It will attempt to load `http://` and `https://` URIs, and infer the content type from the `Content-Type` header.
|
||||
///
|
||||
/// The `image` loader is an [`ImageLoader`][`egui::load::ImageLoader`].
|
||||
/// It will attempt to load any URI with any extension other than `svg`. It will also load any URI without an extension.
|
||||
/// The content type specified by [`BytesPoll::Ready::mime`][`egui::load::BytesPoll::Ready::mime`] always takes precedence.
|
||||
/// This means that even if the URI has a `png` extension, and the `png` image format is enabled, if the content type is
|
||||
/// not one of the supported and enabled image formats, the loader will return [`LoadError::NotSupported`][`egui::load::LoadError::NotSupported`],
|
||||
/// allowing a different loader to attempt to load the image.
|
||||
///
|
||||
/// The `svg` loader is an [`ImageLoader`][`egui::load::ImageLoader`].
|
||||
/// It will attempt to load any URI with an `svg` extension. It will _not_ attempt to load a URI without an extension.
|
||||
/// The content type specified by [`BytesPoll::Ready::mime`][`egui::load::BytesPoll::Ready::mime`] always takes precedence,
|
||||
/// and must include `svg` for it to be considered supported. For example, `image/svg+xml` would be loaded by the `svg` loader.
|
||||
///
|
||||
/// See [`egui::load`] for more information about how loaders work.
|
||||
pub fn install(ctx: &egui::Context) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
ctx.add_bytes_loader(std::sync::Arc::new(self::file_loader::FileLoader::default()));
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "file"))]
|
||||
if !ctx.is_loader_installed(self::file_loader::FileLoader::ID) {
|
||||
ctx.add_bytes_loader(std::sync::Arc::new(self::file_loader::FileLoader::default()));
|
||||
crate::log_trace!("installed FileLoader");
|
||||
}
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
ctx.add_bytes_loader(std::sync::Arc::new(
|
||||
self::ehttp_loader::EhttpLoader::default(),
|
||||
));
|
||||
if !ctx.is_loader_installed(self::ehttp_loader::EhttpLoader::ID) {
|
||||
ctx.add_bytes_loader(std::sync::Arc::new(
|
||||
self::ehttp_loader::EhttpLoader::default(),
|
||||
));
|
||||
crate::log_trace!("installed EhttpLoader");
|
||||
}
|
||||
|
||||
#[cfg(feature = "image")]
|
||||
ctx.add_image_loader(std::sync::Arc::new(
|
||||
self::image_loader::ImageCrateLoader::default(),
|
||||
));
|
||||
if !ctx.is_loader_installed(self::image_loader::ImageCrateLoader::ID) {
|
||||
ctx.add_image_loader(std::sync::Arc::new(
|
||||
self::image_loader::ImageCrateLoader::default(),
|
||||
));
|
||||
crate::log_trace!("installed ImageCrateLoader");
|
||||
}
|
||||
|
||||
#[cfg(feature = "svg")]
|
||||
ctx.add_image_loader(std::sync::Arc::new(self::svg_loader::SvgLoader::default()));
|
||||
if !ctx.is_loader_installed(self::svg_loader::SvgLoader::ID) {
|
||||
ctx.add_image_loader(std::sync::Arc::new(self::svg_loader::SvgLoader::default()));
|
||||
crate::log_trace!("installed SvgLoader");
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
target_arch = "wasm32",
|
||||
any(target_arch = "wasm32", not(feature = "file")),
|
||||
not(feature = "http"),
|
||||
not(feature = "image"),
|
||||
not(feature = "svg")
|
||||
|
|
|
|||
|
|
@ -5,52 +5,60 @@ use egui::{
|
|||
};
|
||||
use std::{sync::Arc, task::Poll};
|
||||
|
||||
type Entry = Poll<Result<Arc<[u8]>, String>>;
|
||||
#[derive(Clone)]
|
||||
struct File {
|
||||
bytes: Arc<[u8]>,
|
||||
mime: Option<String>,
|
||||
}
|
||||
|
||||
impl File {
|
||||
fn from_response(uri: &str, response: ehttp::Response) -> Result<Self, String> {
|
||||
if !response.ok {
|
||||
match response.text() {
|
||||
Some(response_text) => {
|
||||
return Err(format!(
|
||||
"failed to load {uri:?}: {} {} {response_text}",
|
||||
response.status, response.status_text
|
||||
))
|
||||
}
|
||||
None => {
|
||||
return Err(format!(
|
||||
"failed to load {uri:?}: {} {}",
|
||||
response.status, response.status_text
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mime = response.content_type().map(|v| v.to_owned());
|
||||
let bytes = response.bytes.into();
|
||||
|
||||
Ok(File { bytes, mime })
|
||||
}
|
||||
}
|
||||
|
||||
type Entry = Poll<Result<File, String>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EhttpLoader {
|
||||
cache: Arc<Mutex<HashMap<String, Entry>>>,
|
||||
}
|
||||
|
||||
impl EhttpLoader {
|
||||
pub const ID: &str = egui::generate_loader_id!(EhttpLoader);
|
||||
}
|
||||
|
||||
const PROTOCOLS: &[&str] = &["http://", "https://"];
|
||||
|
||||
fn starts_with_one_of(s: &str, prefixes: &[&str]) -> bool {
|
||||
prefixes.iter().any(|prefix| s.starts_with(prefix))
|
||||
}
|
||||
|
||||
fn get_image_bytes(
|
||||
uri: &str,
|
||||
response: Result<ehttp::Response, String>,
|
||||
) -> Result<Arc<[u8]>, String> {
|
||||
let response = response?;
|
||||
if !response.ok {
|
||||
match response.text() {
|
||||
Some(response_text) => {
|
||||
return Err(format!(
|
||||
"failed to load {uri:?}: {} {} {response_text}",
|
||||
response.status, response.status_text
|
||||
))
|
||||
}
|
||||
None => {
|
||||
return Err(format!(
|
||||
"failed to load {uri:?}: {} {}",
|
||||
response.status, response.status_text
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(content_type) = response.content_type() else {
|
||||
return Err(format!("failed to load {uri:?}: no content-type header found"));
|
||||
};
|
||||
if !content_type.starts_with("image/") {
|
||||
return Err(format!("failed to load {uri:?}: expected content-type starting with \"image/\", found {content_type:?}"));
|
||||
}
|
||||
|
||||
Ok(response.bytes.into())
|
||||
}
|
||||
|
||||
impl BytesLoader for EhttpLoader {
|
||||
fn id(&self) -> &str {
|
||||
Self::ID
|
||||
}
|
||||
|
||||
fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult {
|
||||
if !starts_with_one_of(uri, PROTOCOLS) {
|
||||
return Err(LoadError::NotSupported);
|
||||
|
|
@ -59,9 +67,10 @@ impl BytesLoader for EhttpLoader {
|
|||
let mut cache = self.cache.lock();
|
||||
if let Some(entry) = cache.get(uri).cloned() {
|
||||
match entry {
|
||||
Poll::Ready(Ok(bytes)) => Ok(BytesPoll::Ready {
|
||||
Poll::Ready(Ok(file)) => Ok(BytesPoll::Ready {
|
||||
size: None,
|
||||
bytes: Bytes::Shared(bytes),
|
||||
bytes: Bytes::Shared(file.bytes),
|
||||
mime: file.mime,
|
||||
}),
|
||||
Poll::Ready(Err(err)) => Err(LoadError::Custom(err)),
|
||||
Poll::Pending => Ok(BytesPoll::Pending { size: None }),
|
||||
|
|
@ -77,7 +86,14 @@ impl BytesLoader for EhttpLoader {
|
|||
let ctx = ctx.clone();
|
||||
let cache = self.cache.clone();
|
||||
move |response| {
|
||||
let result = get_image_bytes(&uri, response);
|
||||
let result = match response {
|
||||
Ok(response) => File::from_response(&uri, response),
|
||||
Err(err) => {
|
||||
// Log details; return summary
|
||||
crate::log_err!("Failed to load {uri:?}: {err}");
|
||||
Err(format!("Failed to load {uri:?}"))
|
||||
}
|
||||
};
|
||||
crate::log_trace!("finished loading {uri:?}");
|
||||
let prev = cache.lock().insert(uri, Poll::Ready(result));
|
||||
assert!(matches!(prev, Some(Poll::Pending)));
|
||||
|
|
@ -93,12 +109,18 @@ impl BytesLoader for EhttpLoader {
|
|||
let _ = self.cache.lock().remove(uri);
|
||||
}
|
||||
|
||||
fn forget_all(&self) {
|
||||
self.cache.lock().clear();
|
||||
}
|
||||
|
||||
fn byte_size(&self) -> usize {
|
||||
self.cache
|
||||
.lock()
|
||||
.values()
|
||||
.map(|entry| match entry {
|
||||
Poll::Ready(Ok(bytes)) => bytes.len(),
|
||||
Poll::Ready(Ok(file)) => {
|
||||
file.bytes.len() + file.mime.as_ref().map_or(0, |m| m.len())
|
||||
}
|
||||
Poll::Ready(Err(err)) => err.len(),
|
||||
_ => 0,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,7 +5,13 @@ use egui::{
|
|||
};
|
||||
use std::{sync::Arc, task::Poll, thread};
|
||||
|
||||
type Entry = Poll<Result<Arc<[u8]>, String>>;
|
||||
#[derive(Clone)]
|
||||
struct File {
|
||||
bytes: Arc<[u8]>,
|
||||
mime: Option<String>,
|
||||
}
|
||||
|
||||
type Entry = Poll<Result<File, String>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FileLoader {
|
||||
|
|
@ -13,9 +19,17 @@ pub struct FileLoader {
|
|||
cache: Arc<Mutex<HashMap<String, Entry>>>,
|
||||
}
|
||||
|
||||
impl FileLoader {
|
||||
pub const ID: &str = egui::generate_loader_id!(FileLoader);
|
||||
}
|
||||
|
||||
const PROTOCOL: &str = "file://";
|
||||
|
||||
impl BytesLoader for FileLoader {
|
||||
fn id(&self) -> &str {
|
||||
Self::ID
|
||||
}
|
||||
|
||||
fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult {
|
||||
// File loader only supports the `file` protocol.
|
||||
let Some(path) = uri.strip_prefix(PROTOCOL) else {
|
||||
|
|
@ -26,9 +40,10 @@ impl BytesLoader for FileLoader {
|
|||
if let Some(entry) = cache.get(path).cloned() {
|
||||
// `path` has either begun loading, is loaded, or has failed to load.
|
||||
match entry {
|
||||
Poll::Ready(Ok(bytes)) => Ok(BytesPoll::Ready {
|
||||
Poll::Ready(Ok(file)) => Ok(BytesPoll::Ready {
|
||||
size: None,
|
||||
bytes: Bytes::Shared(bytes),
|
||||
bytes: Bytes::Shared(file.bytes),
|
||||
mime: file.mime,
|
||||
}),
|
||||
Poll::Ready(Err(err)) => Err(LoadError::Custom(err)),
|
||||
Poll::Pending => Ok(BytesPoll::Pending { size: None }),
|
||||
|
|
@ -51,7 +66,12 @@ impl BytesLoader for FileLoader {
|
|||
let uri = uri.to_owned();
|
||||
move || {
|
||||
let result = match std::fs::read(&path) {
|
||||
Ok(bytes) => Ok(bytes.into()),
|
||||
Ok(bytes) => Ok(File {
|
||||
bytes: bytes.into(),
|
||||
mime: mime_guess::from_path(&path)
|
||||
.first_raw()
|
||||
.map(|v| v.to_owned()),
|
||||
}),
|
||||
Err(err) => Err(err.to_string()),
|
||||
};
|
||||
let prev = cache.lock().insert(path, Poll::Ready(result));
|
||||
|
|
@ -70,12 +90,18 @@ impl BytesLoader for FileLoader {
|
|||
let _ = self.cache.lock().remove(uri);
|
||||
}
|
||||
|
||||
fn forget_all(&self) {
|
||||
self.cache.lock().clear();
|
||||
}
|
||||
|
||||
fn byte_size(&self) -> usize {
|
||||
self.cache
|
||||
.lock()
|
||||
.values()
|
||||
.map(|entry| match entry {
|
||||
Poll::Ready(Ok(bytes)) => bytes.len(),
|
||||
Poll::Ready(Ok(file)) => {
|
||||
file.bytes.len() + file.mime.as_ref().map_or(0, |m| m.len())
|
||||
}
|
||||
Poll::Ready(Err(err)) => err.len(),
|
||||
_ => 0,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ pub struct ImageCrateLoader {
|
|||
cache: Mutex<HashMap<String, Entry>>,
|
||||
}
|
||||
|
||||
fn is_supported(uri: &str) -> bool {
|
||||
impl ImageCrateLoader {
|
||||
pub const ID: &str = egui::generate_loader_id!(ImageCrateLoader);
|
||||
}
|
||||
|
||||
fn is_supported_uri(uri: &str) -> bool {
|
||||
let Some(ext) = Path::new(uri).extension().and_then(|ext| ext.to_str()) else {
|
||||
// `true` because if there's no extension, assume that we support it
|
||||
return true
|
||||
|
|
@ -22,9 +26,23 @@ fn is_supported(uri: &str) -> bool {
|
|||
ext != "svg"
|
||||
}
|
||||
|
||||
fn is_unsupported_mime(mime: &str) -> bool {
|
||||
mime.contains("svg")
|
||||
}
|
||||
|
||||
impl ImageLoader for ImageCrateLoader {
|
||||
fn id(&self) -> &str {
|
||||
Self::ID
|
||||
}
|
||||
|
||||
fn load(&self, ctx: &egui::Context, uri: &str, _: SizeHint) -> ImageLoadResult {
|
||||
if !is_supported(uri) {
|
||||
// three stages of guessing if we support loading the image:
|
||||
// 1. URI extension
|
||||
// 2. Mime from `BytesPoll::Ready`
|
||||
// 3. image::guess_format
|
||||
|
||||
// (1)
|
||||
if !is_supported_uri(uri) {
|
||||
return Err(LoadError::NotSupported);
|
||||
}
|
||||
|
||||
|
|
@ -36,7 +54,14 @@ impl ImageLoader for ImageCrateLoader {
|
|||
}
|
||||
} else {
|
||||
match ctx.try_load_bytes(uri) {
|
||||
Ok(BytesPoll::Ready { bytes, .. }) => {
|
||||
Ok(BytesPoll::Ready { bytes, mime, .. }) => {
|
||||
// (2 and 3)
|
||||
if mime.as_deref().is_some_and(is_unsupported_mime)
|
||||
|| image::guess_format(&bytes).is_err()
|
||||
{
|
||||
return Err(LoadError::NotSupported);
|
||||
}
|
||||
|
||||
crate::log_trace!("started loading {uri:?}");
|
||||
let result = crate::image::load_image_bytes(&bytes).map(Arc::new);
|
||||
crate::log_trace!("finished loading {uri:?}");
|
||||
|
|
@ -56,6 +81,10 @@ impl ImageLoader for ImageCrateLoader {
|
|||
let _ = self.cache.lock().remove(uri);
|
||||
}
|
||||
|
||||
fn forget_all(&self) {
|
||||
self.cache.lock().clear();
|
||||
}
|
||||
|
||||
fn byte_size(&self) -> usize {
|
||||
self.cache
|
||||
.lock()
|
||||
|
|
@ -74,11 +103,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn check_support() {
|
||||
assert!(is_supported("https://test.png"));
|
||||
assert!(is_supported("test.jpeg"));
|
||||
assert!(is_supported("http://test.gif"));
|
||||
assert!(is_supported("test.webp"));
|
||||
assert!(is_supported("file://test"));
|
||||
assert!(!is_supported("test.svg"));
|
||||
assert!(is_supported_uri("https://test.png"));
|
||||
assert!(is_supported_uri("test.jpeg"));
|
||||
assert!(is_supported_uri("http://test.gif"));
|
||||
assert!(is_supported_uri("test.webp"));
|
||||
assert!(is_supported_uri("file://test"));
|
||||
assert!(!is_supported_uri("test.svg"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ pub struct SvgLoader {
|
|||
cache: Mutex<HashMap<(String, SizeHint), Entry>>,
|
||||
}
|
||||
|
||||
impl SvgLoader {
|
||||
pub const ID: &str = egui::generate_loader_id!(SvgLoader);
|
||||
}
|
||||
|
||||
fn is_supported(uri: &str) -> bool {
|
||||
let Some(ext) = Path::new(uri).extension().and_then(|ext| ext.to_str()) else { return false };
|
||||
|
||||
|
|
@ -20,6 +24,10 @@ fn is_supported(uri: &str) -> bool {
|
|||
}
|
||||
|
||||
impl ImageLoader for SvgLoader {
|
||||
fn id(&self) -> &str {
|
||||
Self::ID
|
||||
}
|
||||
|
||||
fn load(&self, ctx: &egui::Context, uri: &str, size_hint: SizeHint) -> ImageLoadResult {
|
||||
if !is_supported(uri) {
|
||||
return Err(LoadError::NotSupported);
|
||||
|
|
@ -39,7 +47,7 @@ impl ImageLoader for SvgLoader {
|
|||
Ok(BytesPoll::Ready { bytes, .. }) => {
|
||||
crate::log_trace!("started loading {uri:?}");
|
||||
let fit_to = match size_hint {
|
||||
SizeHint::Original => usvg::FitTo::Original,
|
||||
SizeHint::Scale(factor) => usvg::FitTo::Zoom(factor.into_inner()),
|
||||
SizeHint::Width(w) => usvg::FitTo::Width(w),
|
||||
SizeHint::Height(h) => usvg::FitTo::Height(h),
|
||||
SizeHint::Size(w, h) => usvg::FitTo::Size(w, h),
|
||||
|
|
@ -63,6 +71,10 @@ impl ImageLoader for SvgLoader {
|
|||
self.cache.lock().retain(|(u, _), _| u != uri);
|
||||
}
|
||||
|
||||
fn forget_all(&self) {
|
||||
self.cache.lock().clear();
|
||||
}
|
||||
|
||||
fn byte_size(&self) -> usize {
|
||||
self.cache
|
||||
.lock()
|
||||
|
|
|
|||
|
|
@ -1234,7 +1234,7 @@ impl PlotItem for PlotImage {
|
|||
Rect::from_two_pos(left_top_screen, right_bottom_screen)
|
||||
};
|
||||
let screen_rotation = -*rotation as f32;
|
||||
Image::new(*texture_id, image_screen_rect.size())
|
||||
RawImage::new((*texture_id, image_screen_rect.size()))
|
||||
.bg_fill(*bg_fill)
|
||||
.tint(*tint)
|
||||
.uv(*uv)
|
||||
|
|
|
|||
|
|
@ -86,7 +86,10 @@ impl TextureHandle {
|
|||
|
||||
/// width x height
|
||||
pub fn size(&self) -> [usize; 2] {
|
||||
self.tex_mngr.read().meta(self.id).unwrap().size
|
||||
self.tex_mngr
|
||||
.read()
|
||||
.meta(self.id)
|
||||
.map_or([0, 0], |tex| tex.size)
|
||||
}
|
||||
|
||||
/// width x height
|
||||
|
|
@ -97,7 +100,10 @@ impl TextureHandle {
|
|||
|
||||
/// `width x height x bytes_per_pixel`
|
||||
pub fn byte_size(&self) -> usize {
|
||||
self.tex_mngr.read().meta(self.id).unwrap().bytes_used()
|
||||
self.tex_mngr
|
||||
.read()
|
||||
.meta(self.id)
|
||||
.map_or(0, |tex| tex.bytes_used())
|
||||
}
|
||||
|
||||
/// width / height
|
||||
|
|
@ -108,7 +114,10 @@ impl TextureHandle {
|
|||
|
||||
/// Debug-name.
|
||||
pub fn name(&self) -> String {
|
||||
self.tex_mngr.read().meta(self.id).unwrap().name.clone()
|
||||
self.tex_mngr
|
||||
.read()
|
||||
.meta(self.id)
|
||||
.map_or_else(|| "<none>".to_owned(), |tex| tex.name.clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,16 @@ use std::hash::{Hash, Hasher};
|
|||
/// Possible types for `T` are `f32` and `f64`.
|
||||
///
|
||||
/// See also [`FloatOrd`].
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct OrderedFloat<T>(T);
|
||||
|
||||
impl<T: Float + Copy> OrderedFloat<T> {
|
||||
#[inline]
|
||||
pub fn into_inner(self) -> T {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Float> Eq for OrderedFloat<T> {}
|
||||
|
||||
impl<T: Float> PartialEq<Self> for OrderedFloat<T> {
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
Example how to download and show an image with eframe/egui.
|
||||
|
||||
```sh
|
||||
cargo run -p download_image
|
||||
```
|
||||
|
||||

|
||||
|
Before Width: | Height: | Size: 353 KiB |
|
|
@ -1,41 +0,0 @@
|
|||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
|
||||
use eframe::egui;
|
||||
|
||||
fn main() -> Result<(), eframe::Error> {
|
||||
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
||||
let options = eframe::NativeOptions::default();
|
||||
eframe::run_native(
|
||||
"Download and show an image with eframe/egui",
|
||||
options,
|
||||
Box::new(|cc| {
|
||||
// Without the following call, the `Image2` created below
|
||||
// will simply output `not supported` error messages.
|
||||
egui_extras::loaders::install(&cc.egui_ctx);
|
||||
Box::new(MyApp)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MyApp;
|
||||
|
||||
impl eframe::App for MyApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
let width = ui.available_width();
|
||||
let half_height = ui.available_height() / 2.0;
|
||||
|
||||
ui.allocate_ui(egui::Vec2::new(width, half_height), |ui| {
|
||||
ui.add(egui::Image2::from_uri(
|
||||
"https://picsum.photos/seed/1.759706314/1024",
|
||||
))
|
||||
});
|
||||
ui.allocate_ui(egui::Vec2::new(width, half_height), |ui| {
|
||||
ui.add(egui::Image2::from_uri(
|
||||
"https://this-is-hopefully-not-a-real-website.rs/image.png",
|
||||
))
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "download_image"
|
||||
name = "images"
|
||||
version = "0.1.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
authors = ["Jan Procházka <github.com/jprochazk>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
|
|
@ -10,12 +10,14 @@ publish = false
|
|||
|
||||
[dependencies]
|
||||
eframe = { path = "../../crates/eframe", features = [
|
||||
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
|
||||
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
|
||||
] }
|
||||
egui_extras = { path = "../../crates/egui_extras", features = [
|
||||
"http",
|
||||
"image",
|
||||
"log",
|
||||
"all-loaders",
|
||||
"log",
|
||||
] }
|
||||
env_logger = "0.10"
|
||||
image = { version = "0.24", default-features = false, features = ["jpeg"] }
|
||||
image = { version = "0.24", default-features = false, features = [
|
||||
"jpeg",
|
||||
"png",
|
||||
] }
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
Example showing how to show images with eframe/egui.
|
||||
|
||||
```sh
|
||||
cargo run -p images
|
||||
```
|
||||
|
||||

|
||||
|
After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
|
@ -0,0 +1,41 @@
|
|||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
|
||||
use eframe::egui;
|
||||
use eframe::epaint::vec2;
|
||||
|
||||
fn main() -> Result<(), eframe::Error> {
|
||||
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
||||
let options = eframe::NativeOptions {
|
||||
drag_and_drop_support: true,
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native(
|
||||
"Image Viewer",
|
||||
options,
|
||||
Box::new(|cc| {
|
||||
// The following call is needed to load images when using `ui.image` and `egui::Image`:
|
||||
egui_extras::loaders::install(&cc.egui_ctx);
|
||||
Box::<MyApp>::default()
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MyApp {}
|
||||
|
||||
impl eframe::App for MyApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::ScrollArea::new([true, true]).show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::Image::new(egui::include_image!("ferris.svg"))
|
||||
.fit_to_fraction(vec2(1.0, 0.5)),
|
||||
);
|
||||
ui.add(
|
||||
egui::Image::new("https://picsum.photos/seed/1.759706314/1024".into())
|
||||
.rounding(egui::Rounding::same(10.0)),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
[package]
|
||||
name = "retained_image"
|
||||
version = "0.1.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
|
||||
[dependencies]
|
||||
eframe = { path = "../../crates/eframe", features = [
|
||||
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
|
||||
] }
|
||||
egui_extras = { path = "../../crates/egui_extras", features = ["image", "log"] }
|
||||
env_logger = "0.10"
|
||||
image = { version = "0.24", default-features = false, features = ["png"] }
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
Example how to show an image with eframe/egui.
|
||||
|
||||
```sh
|
||||
cargo run -p retained_image
|
||||
```
|
||||
|
||||

|
||||
|
Before Width: | Height: | Size: 301 KiB |
|
Before Width: | Height: | Size: 140 KiB |
|
|
@ -1,84 +0,0 @@
|
|||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
|
||||
use eframe::egui;
|
||||
use egui_extras::RetainedImage;
|
||||
|
||||
fn main() -> Result<(), eframe::Error> {
|
||||
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
||||
let options = eframe::NativeOptions {
|
||||
initial_window_size: Some(egui::vec2(400.0, 1000.0)),
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native(
|
||||
"Show an image with eframe/egui",
|
||||
options,
|
||||
Box::new(|_cc| Box::<MyApp>::default()),
|
||||
)
|
||||
}
|
||||
|
||||
struct MyApp {
|
||||
image: RetainedImage,
|
||||
rounding: f32,
|
||||
tint: egui::Color32,
|
||||
}
|
||||
|
||||
impl Default for MyApp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// crab image is CC0, found on https://stocksnap.io/search/crab
|
||||
image: RetainedImage::from_image_bytes("crab.png", include_bytes!("crab.png")).unwrap(),
|
||||
rounding: 32.0,
|
||||
tint: egui::Color32::from_rgb(100, 200, 200),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for MyApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
let Self {
|
||||
image,
|
||||
rounding,
|
||||
tint,
|
||||
} = self;
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.heading("This is an image:");
|
||||
image.show(ui);
|
||||
|
||||
ui.add_space(32.0);
|
||||
|
||||
ui.heading("This is a tinted image with rounded corners:");
|
||||
ui.add(
|
||||
egui::Image::new(image.texture_id(ctx), image.size_vec2())
|
||||
.tint(*tint)
|
||||
.rounding(*rounding),
|
||||
);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Tint:");
|
||||
egui::color_picker::color_edit_button_srgba(
|
||||
ui,
|
||||
tint,
|
||||
egui::color_picker::Alpha::BlendOrAdditive,
|
||||
);
|
||||
|
||||
ui.add_space(16.0);
|
||||
|
||||
ui.label("Rounding:");
|
||||
ui.add(
|
||||
egui::DragValue::new(rounding)
|
||||
.speed(1.0)
|
||||
.clamp_range(0.0..=0.5 * image.size_vec2().min_elem()),
|
||||
);
|
||||
});
|
||||
|
||||
ui.add_space(32.0);
|
||||
|
||||
ui.heading("This is an image you can click:");
|
||||
ui.add(egui::ImageButton::new(
|
||||
image.texture_id(ctx),
|
||||
image.size_vec2(),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ impl eframe::App for MyApp {
|
|||
});
|
||||
|
||||
if let Some(texture) = self.texture.as_ref() {
|
||||
ui.image(texture, ui.available_size());
|
||||
ui.raw_image((texture.id(), ui.available_size()));
|
||||
} else {
|
||||
ui.spinner();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
[package]
|
||||
name = "svg"
|
||||
version = "0.1.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
publish = false
|
||||
|
||||
|
||||
[dependencies]
|
||||
eframe = { path = "../../crates/eframe", features = [
|
||||
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
|
||||
] }
|
||||
egui_extras = { path = "../../crates/egui_extras", features = ["log", "svg"] }
|
||||
env_logger = "0.10"
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
Example how to show an SVG image.
|
||||
|
||||
```sh
|
||||
cargo run -p svg
|
||||
```
|
||||
|
||||

|
||||
|
Before Width: | Height: | Size: 142 KiB |
|
|
@ -1,47 +0,0 @@
|
|||
//! A good way of displaying an SVG image in egui.
|
||||
//!
|
||||
//! Requires the dependency `egui_extras` with the `svg` feature.
|
||||
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
|
||||
use eframe::egui;
|
||||
|
||||
fn main() -> Result<(), eframe::Error> {
|
||||
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
||||
let options = eframe::NativeOptions {
|
||||
initial_window_size: Some(egui::vec2(1000.0, 700.0)),
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native(
|
||||
"svg example",
|
||||
options,
|
||||
Box::new(|cc| {
|
||||
// Without the following call, the `Image2` created below
|
||||
// will simply output a `not supported` error message.
|
||||
egui_extras::loaders::install(&cc.egui_ctx);
|
||||
Box::new(MyApp)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
struct MyApp;
|
||||
|
||||
impl eframe::App for MyApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.heading("SVG example");
|
||||
ui.label("The SVG is rasterized and displayed as a texture.");
|
||||
|
||||
ui.separator();
|
||||
|
||||
let max_size = ui.available_size();
|
||||
ui.add(
|
||||
egui::Image2::from_static_bytes(
|
||||
"ferris.svg",
|
||||
include_bytes!("rustacean-flat-happy.svg"),
|
||||
)
|
||||
.size_hint(max_size),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
Rust logo by Mozilla, from https://github.com/rust-lang/rust-artwork
|
||||