Simplify http demo and add new download_image eframe demo
This commit is contained in:
parent
66d80e2519
commit
b2c8cd0867
6 changed files with 164 additions and 87 deletions
30
Cargo.lock
generated
30
Cargo.lock
generated
|
@ -783,8 +783,10 @@ dependencies = [
|
|||
"egui_glium",
|
||||
"egui_glow",
|
||||
"egui_web",
|
||||
"ehttp",
|
||||
"epi",
|
||||
"image",
|
||||
"poll-promise",
|
||||
"rfd",
|
||||
]
|
||||
|
||||
|
@ -832,6 +834,7 @@ dependencies = [
|
|||
"enum-map",
|
||||
"epi",
|
||||
"image",
|
||||
"poll-promise",
|
||||
"serde",
|
||||
"syntect",
|
||||
"unicode_names2",
|
||||
|
@ -885,9 +888,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ehttp"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b078e2305de4c998700ac152b3e7a358d7fbe77e15b3b1cd2c44a8b82176124f"
|
||||
checksum = "80b69a6f9168b96c0ae04763bec27a8b06b34343c334dd2703a4ec21f0f5e110"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"ureq",
|
||||
|
@ -913,9 +916,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "enum-map"
|
||||
version = "1.1.1"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e893a7ba6116821058dec84a6fb14fb2a97cd8ce5fd0f85d5a4e760ecd7329d9"
|
||||
checksum = "9ec3484df47a85c121b9d8fbf265ca7eedc26a5c4c341db7cf800876201c766f"
|
||||
dependencies = [
|
||||
"enum-map-derive",
|
||||
"serde",
|
||||
|
@ -923,9 +926,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "enum-map-derive"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84278eae0af6e34ff6c1db44c11634a694aafac559ff3080e4db4e4ac35907aa"
|
||||
checksum = "8182c0d26a908f001a23adc388fcef7fde884fbaf668874126cd5a3c13ca299e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -1903,6 +1906,15 @@ dependencies = [
|
|||
"miniz_oxide 0.3.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "poll-promise"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "260817b339544e5b23d4bb66d4522d89dd64af88d16b6dcd7b2a2409ee2e095d"
|
||||
dependencies = [
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "1.1.0"
|
||||
|
@ -2274,6 +2286,12 @@ version = "0.5.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.8.0"
|
||||
|
|
|
@ -38,7 +38,9 @@ egui_glow = { version = "0.16.0", path = "../egui_glow", default-features = fals
|
|||
egui_web = { version = "0.16.0", path = "../egui_web", default-features = false, features = ["glow"] }
|
||||
|
||||
[dev-dependencies]
|
||||
image = { version = "0.23", default-features = false, features = ["png"] }
|
||||
ehttp = "0.2"
|
||||
image = { version = "0.23", default-features = false, features = ["jpeg", "png"] }
|
||||
poll-promise = "0.1"
|
||||
rfd = "0.6"
|
||||
|
||||
[features]
|
||||
|
|
82
eframe/examples/download_image.rs
Normal file
82
eframe/examples/download_image.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
|
||||
use eframe::{egui, epi};
|
||||
use poll_promise::Promise;
|
||||
|
||||
fn main() {
|
||||
let options = eframe::NativeOptions::default();
|
||||
eframe::run_native(Box::new(MyApp::default()), options);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MyApp {
|
||||
/// `None` when download hasn't started yet.
|
||||
promise: Option<Promise<ehttp::Result<egui::TextureHandle>>>,
|
||||
}
|
||||
|
||||
impl epi::App for MyApp {
|
||||
fn name(&self) -> &str {
|
||||
"Download and show an image with eframe/egui"
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
|
||||
let promise = self.promise.get_or_insert_with(|| {
|
||||
// Begin download.
|
||||
// We download the image using `ehttp`, a library that works both in WASM and on native.
|
||||
// We use the `poll-promise` library to communicate with the UI thread.
|
||||
let ctx = ctx.clone();
|
||||
let frame = frame.clone();
|
||||
let (sender, promise) = Promise::new();
|
||||
let request = ehttp::Request::get("https://picsum.photos/seed/1.759706314/1024");
|
||||
ehttp::fetch(request, move |response| {
|
||||
frame.request_repaint(); // wake up UI thread
|
||||
let texture = response.and_then(|response| parse_response(&ctx, response));
|
||||
sender.send(texture); // send the results back to the UI thread.
|
||||
});
|
||||
promise
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| match promise.ready() {
|
||||
None => {
|
||||
ui.add(egui::Spinner::new()); // still loading
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
ui.colored_label(egui::Color32::RED, err); // something went wrong
|
||||
}
|
||||
Some(Ok(texture)) => {
|
||||
let mut size = texture.size_vec2();
|
||||
size *= (ui.available_width() / size.x).min(1.0);
|
||||
size *= (ui.available_height() / size.y).min(1.0);
|
||||
ui.image(texture, size);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_response(
|
||||
ctx: &egui::Context,
|
||||
response: ehttp::Response,
|
||||
) -> Result<egui::TextureHandle, String> {
|
||||
let content_type = response.content_type().unwrap_or_default();
|
||||
if content_type.starts_with("image/") {
|
||||
let image = load_image(&response.bytes).map_err(|err| err.to_string())?;
|
||||
Ok(ctx.load_texture("my-image", image))
|
||||
} else {
|
||||
Err(format!(
|
||||
"Expected image, found content-type {:?}",
|
||||
content_type
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn load_image(image_data: &[u8]) -> Result<egui::ColorImage, image::ImageError> {
|
||||
use image::GenericImageView as _;
|
||||
let image = image::load_from_memory(image_data)?;
|
||||
let size = [image.width() as _, image.height() as _];
|
||||
let image_buffer = image.to_rgba8();
|
||||
let pixels = image_buffer.as_flat_samples();
|
||||
Ok(egui::ColorImage::from_rgba_unmultiplied(
|
||||
size,
|
||||
pixels.as_slice(),
|
||||
))
|
||||
}
|
|
@ -1044,6 +1044,7 @@ impl Context {
|
|||
textures.len(),
|
||||
bytes as f64 * 1e-6
|
||||
));
|
||||
let max_preview_size = Vec2::new(48.0, 32.0);
|
||||
|
||||
ui.group(|ui| {
|
||||
ScrollArea::vertical()
|
||||
|
@ -1053,14 +1054,24 @@ impl Context {
|
|||
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
|
||||
Grid::new("textures")
|
||||
.striped(true)
|
||||
.num_columns(3)
|
||||
.num_columns(4)
|
||||
.spacing(Vec2::new(16.0, 2.0))
|
||||
.min_row_height(max_preview_size.y)
|
||||
.show(ui, |ui| {
|
||||
for (_id, texture) in &textures {
|
||||
let [w, h] = texture.size;
|
||||
for (&texture_id, meta) in textures {
|
||||
let [w, h] = meta.size;
|
||||
|
||||
let mut size = Vec2::new(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 full size on hover
|
||||
ui.image(texture_id, Vec2::new(w as f32, h as f32));
|
||||
});
|
||||
|
||||
ui.label(format!("{} x {}", w, h));
|
||||
ui.label(format!("{:.3} MB", texture.bytes_used() as f64 * 1e-6));
|
||||
ui.label(format!("{:?}", texture.name));
|
||||
ui.label(format!("{:.3} MB", meta.bytes_used() as f64 * 1e-6));
|
||||
ui.label(format!("{:?}", meta.name));
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -28,12 +28,13 @@ egui = { version = "0.16.0", path = "../egui", default-features = false }
|
|||
epi = { version = "0.16.0", path = "../epi" }
|
||||
|
||||
chrono = { version = "0.4", features = ["js-sys", "wasmbind"], optional = true }
|
||||
enum-map = { version = "1", features = ["serde"] }
|
||||
enum-map = { version = "2", features = ["serde"] }
|
||||
unicode_names2 = { version = "0.4.0", default-features = false }
|
||||
|
||||
# feature "http":
|
||||
ehttp = { version = "0.1.0", optional = true }
|
||||
ehttp = { version = "0.2.0", optional = true }
|
||||
image = { version = "0.23", default-features = false, features = ["jpeg", "png"], optional = true }
|
||||
poll-promise = { version = "0.1", default-features = false, optional = true }
|
||||
|
||||
# feature "syntax_highlighting":
|
||||
syntect = { version = "4", default-features = false, features = ["default-fancy"], optional = true }
|
||||
|
@ -52,7 +53,7 @@ extra_debug_asserts = ["egui/extra_debug_asserts"]
|
|||
# Always enable additional checks.
|
||||
extra_asserts = ["egui/extra_asserts"]
|
||||
|
||||
http = ["ehttp", "image"]
|
||||
http = ["ehttp", "image", "poll-promise"]
|
||||
persistence = ["egui/persistence", "epi/persistence", "serde"]
|
||||
serialize = ["egui/serialize", "serde"]
|
||||
syntax_highlighting = ["syntect"]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::sync::mpsc::Receiver;
|
||||
use poll_promise::Promise;
|
||||
|
||||
struct Resource {
|
||||
/// HTTP response
|
||||
|
@ -7,7 +7,7 @@ struct Resource {
|
|||
text: Option<String>,
|
||||
|
||||
/// If set, the response was an image.
|
||||
image: Option<egui::ImageData>,
|
||||
texture: Option<egui::TextureHandle>,
|
||||
|
||||
/// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md").
|
||||
colored_text: Option<ColoredText>,
|
||||
|
@ -17,21 +17,21 @@ impl Resource {
|
|||
fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self {
|
||||
let content_type = response.content_type().unwrap_or_default();
|
||||
let image = if content_type.starts_with("image/") {
|
||||
load_image(&response.bytes).ok().map(|img| img.into())
|
||||
load_image(&response.bytes).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let text = response.text();
|
||||
let texture = image.map(|image| ctx.load_texture(&response.url, image));
|
||||
|
||||
let colored_text = text
|
||||
.as_ref()
|
||||
.and_then(|text| syntax_highlighting(ctx, &response, text));
|
||||
let text = response.text();
|
||||
let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text));
|
||||
let text = text.map(|text| text.to_owned());
|
||||
|
||||
Self {
|
||||
response,
|
||||
text,
|
||||
image,
|
||||
texture,
|
||||
colored_text,
|
||||
}
|
||||
}
|
||||
|
@ -42,22 +42,14 @@ pub struct HttpApp {
|
|||
url: String,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
in_progress: Option<Receiver<Result<ehttp::Response, String>>>,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
result: Option<Result<Resource, String>>,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
tex_mngr: TexMngr,
|
||||
promise: Option<Promise<ehttp::Result<Resource>>>,
|
||||
}
|
||||
|
||||
impl Default for HttpApp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
url: "https://raw.githubusercontent.com/emilk/egui/master/README.md".to_owned(),
|
||||
in_progress: Default::default(),
|
||||
result: Default::default(),
|
||||
tex_mngr: Default::default(),
|
||||
promise: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -68,14 +60,6 @@ impl epi::App for HttpApp {
|
|||
}
|
||||
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
|
||||
if let Some(receiver) = &mut self.in_progress {
|
||||
// Are we there yet?
|
||||
if let Ok(result) = receiver.try_recv() {
|
||||
self.in_progress = None;
|
||||
self.result = Some(result.map(|response| Resource::from_response(ctx, response)));
|
||||
}
|
||||
}
|
||||
|
||||
egui::TopBottomPanel::bottom("http_bottom").show(ctx, |ui| {
|
||||
let layout = egui::Layout::top_down(egui::Align::Center).with_main_justify(true);
|
||||
ui.allocate_ui_with_layout(ui.available_size(), layout, |ui| {
|
||||
|
@ -94,33 +78,36 @@ impl epi::App for HttpApp {
|
|||
});
|
||||
|
||||
if trigger_fetch {
|
||||
let request = ehttp::Request::get(&self.url);
|
||||
let ctx = ctx.clone();
|
||||
let frame = frame.clone();
|
||||
let (sender, receiver) = std::sync::mpsc::channel();
|
||||
self.in_progress = Some(receiver);
|
||||
|
||||
let (sender, promise) = Promise::new();
|
||||
let request = ehttp::Request::get(&self.url);
|
||||
ehttp::fetch(request, move |response| {
|
||||
sender.send(response).ok();
|
||||
frame.request_repaint();
|
||||
frame.request_repaint(); // wake up UI thread
|
||||
let resource = response.map(|response| Resource::from_response(&ctx, response));
|
||||
sender.send(resource);
|
||||
});
|
||||
self.promise = Some(promise);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
if self.in_progress.is_some() {
|
||||
ui.label("Please wait…");
|
||||
} else if let Some(result) = &self.result {
|
||||
match result {
|
||||
Ok(resource) => {
|
||||
ui_resource(ui, &mut self.tex_mngr, resource);
|
||||
}
|
||||
Err(error) => {
|
||||
// This should only happen if the fetch API isn't available or something similar.
|
||||
ui.colored_label(
|
||||
egui::Color32::RED,
|
||||
if error.is_empty() { "Error" } else { error },
|
||||
);
|
||||
if let Some(promise) = &self.promise {
|
||||
if let Some(result) = promise.ready() {
|
||||
match result {
|
||||
Ok(resource) => {
|
||||
ui_resource(ui, resource);
|
||||
}
|
||||
Err(error) => {
|
||||
// This should only happen if the fetch API isn't available or something similar.
|
||||
ui.colored_label(
|
||||
egui::Color32::RED,
|
||||
if error.is_empty() { "Error" } else { error },
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.add(egui::Spinner::new());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -160,11 +147,11 @@ fn ui_url(ui: &mut egui::Ui, frame: &epi::Frame, url: &mut String) -> bool {
|
|||
trigger_fetch
|
||||
}
|
||||
|
||||
fn ui_resource(ui: &mut egui::Ui, tex_mngr: &mut TexMngr, resource: &Resource) {
|
||||
fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
|
||||
let Resource {
|
||||
response,
|
||||
text,
|
||||
image,
|
||||
texture,
|
||||
colored_text,
|
||||
} = resource;
|
||||
|
||||
|
@ -211,8 +198,7 @@ fn ui_resource(ui: &mut egui::Ui, tex_mngr: &mut TexMngr, resource: &Resource) {
|
|||
ui.separator();
|
||||
}
|
||||
|
||||
if let Some(image) = image {
|
||||
let texture = tex_mngr.texture(ui.ctx(), &response.url, image);
|
||||
if let Some(texture) = texture {
|
||||
let mut size = texture.size_vec2();
|
||||
size *= (ui.available_width() / size.x).min(1.0);
|
||||
ui.image(texture, size);
|
||||
|
@ -286,29 +272,6 @@ impl ColoredText {
|
|||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Texture/image handling is very manual at the moment.
|
||||
|
||||
/// Immediate mode texture manager that supports at most one texture at the time :)
|
||||
#[derive(Default)]
|
||||
struct TexMngr {
|
||||
loaded_url: String,
|
||||
texture: Option<egui::TextureHandle>,
|
||||
}
|
||||
|
||||
impl TexMngr {
|
||||
fn texture(
|
||||
&mut self,
|
||||
ctx: &egui::Context,
|
||||
url: &str,
|
||||
image: &egui::ImageData,
|
||||
) -> &egui::TextureHandle {
|
||||
if self.loaded_url != url || self.texture.is_none() {
|
||||
self.texture = Some(ctx.load_texture(url, image.clone()));
|
||||
self.loaded_url = url.to_owned();
|
||||
}
|
||||
self.texture.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn load_image(image_data: &[u8]) -> Result<egui::ColorImage, image::ImageError> {
|
||||
use image::GenericImageView as _;
|
||||
|
|
Loading…
Reference in a new issue