mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 06:40:06 -04:00
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>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use egui_extras::RetainedImage;
|
||||
use poll_promise::Promise;
|
||||
|
||||
|
||||
217
crates/egui_demo_app/src/apps/image_viewer.rs
Normal file
217
crates/egui_demo_app/src/apps/image_viewer.rs
Normal file
@@ -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"))]
|
||||
|
||||
Reference in New Issue
Block a user