[example_web] show loading of an image

Required some redesign of `TextureAllocator` as well as
some improvements to the fetch API.
This commit is contained in:
Emil Ernerfeldt 2020-11-18 21:38:29 +01:00
parent 90cecace0c
commit c6ce0b9e8c
16 changed files with 645 additions and 277 deletions

108
Cargo.lock generated
View file

@ -21,6 +21,12 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
[[package]]
name = "adler32"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.4.6" version = "0.4.6"
@ -72,7 +78,7 @@ dependencies = [
"addr2line", "addr2line",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"libc", "libc",
"miniz_oxide", "miniz_oxide 0.4.3",
"object", "object",
"rustc-demangle", "rustc-demangle",
] ]
@ -107,6 +113,12 @@ version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820"
[[package]]
name = "bytemuck"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41aa2ec95ca3b5c54cf73c91acf06d24f4495d5f1b1c12506ae3483d646177ac"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.3.4" version = "1.3.4"
@ -245,6 +257,12 @@ dependencies = [
"objc", "objc",
] ]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]] [[package]]
name = "const_fn" name = "const_fn"
version = "0.4.3" version = "0.4.3"
@ -333,6 +351,15 @@ dependencies = [
"objc", "objc",
] ]
[[package]]
name = "crc32fast"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
dependencies = [
"cfg-if 1.0.0",
]
[[package]] [[package]]
name = "criterion" name = "criterion"
version = "0.3.3" version = "0.3.3"
@ -473,6 +500,16 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "deflate"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174"
dependencies = [
"adler32",
"byteorder",
]
[[package]] [[package]]
name = "demo_glium" name = "demo_glium"
version = "0.1.0" version = "0.1.0"
@ -586,6 +623,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"egui", "egui",
"egui_web", "egui_web",
"image",
"js-sys", "js-sys",
"serde", "serde",
"serde_json", "serde_json",
@ -755,6 +793,22 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "image"
version = "0.23.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4f0a8345b33b082aedec2f4d7d4a926b845cee184cbe78b703413066564431b"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"jpeg-decoder",
"num-iter",
"num-rational",
"num-traits",
"png",
]
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.8" version = "0.1.8"
@ -794,6 +848,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jpeg-decoder"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc797adac5f083b8ff0ca6f6294a999393d76e197c36488e2ef732c4715f6fa3"
dependencies = [
"byteorder",
]
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.45" version = "0.3.45"
@ -905,6 +968,15 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "miniz_oxide"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
dependencies = [
"adler32",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.4.3" version = "0.4.3"
@ -1046,6 +1118,28 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-iter"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.14" version = "0.2.14"
@ -1202,6 +1296,18 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "png"
version = "0.16.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfe7f9f1c730833200b134370e1d5098964231af8450bce9b78ee3ab5278b970"
dependencies = [
"bitflags",
"crc32fast",
"deflate",
"miniz_oxide 0.3.7",
]
[[package]] [[package]]
name = "proc-macro-crate" name = "proc-macro-crate"
version = "0.1.5" version = "0.1.5"

View file

@ -4,7 +4,7 @@ TODO-list for the Egui project. If you looking for something to do, look here.
## Top priority ## Top priority
* Egui-web fetch * Egui-web local storage
## Other ## Other

View file

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<head> <head>
<title>Egui An experimental immediate mode GUI written in Rust</title> <title>Egui Example App</title>
<style> <style>
html { html {
/* Remove touch delay: */ /* Remove touch delay: */

View file

@ -417,6 +417,13 @@ async function init(input) {
var ret = getObject(arg0).writeText(getStringFromWasm0(arg1, arg2)); var ret = getObject(arg0).writeText(getStringFromWasm0(arg1, arg2));
return addHeapObject(ret); return addHeapObject(ret);
}; };
imports.wbg.__wbg_get_d015313eb9359d3a = handleError(function(arg0, arg1, arg2, arg3) {
var ret = getObject(arg1).get(getStringFromWasm0(arg2, arg3));
var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
});
imports.wbg.__wbg_set_e0c72ee4d5eea3d5 = handleError(function(arg0, arg1, arg2, arg3, arg4) { imports.wbg.__wbg_set_e0c72ee4d5eea3d5 = handleError(function(arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).set(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); getObject(arg0).set(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}); });
@ -454,8 +461,12 @@ async function init(input) {
getInt32Memory0()[arg0 / 4 + 1] = len0; getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0; getInt32Memory0()[arg0 / 4 + 0] = ptr0;
}; };
imports.wbg.__wbg_text_966d07536ca6ccdc = handleError(function(arg0) { imports.wbg.__wbg_headers_c736e1fe38752cff = function(arg0) {
var ret = getObject(arg0).text(); var ret = getObject(arg0).headers;
return addHeapObject(ret);
};
imports.wbg.__wbg_arrayBuffer_dc33ab7b8cdf0d63 = handleError(function(arg0) {
var ret = getObject(arg0).arrayBuffer();
return addHeapObject(ret); return addHeapObject(ret);
}); });
imports.wbg.__wbg_now_49847177a6d1d57e = function(arg0) { imports.wbg.__wbg_now_49847177a6d1d57e = function(arg0) {
@ -672,6 +683,9 @@ async function init(input) {
imports.wbg.__wbg_log_3bafd82835c6de6d = function(arg0) { imports.wbg.__wbg_log_3bafd82835c6de6d = function(arg0) {
console.log(getObject(arg0)); console.log(getObject(arg0));
}; };
imports.wbg.__wbg_warn_d05e82888b7fad05 = function(arg0) {
console.warn(getObject(arg0));
};
imports.wbg.__wbg_style_9a41d46c005f7596 = function(arg0) { imports.wbg.__wbg_style_9a41d46c005f7596 = function(arg0) {
var ret = getObject(arg0).style; var ret = getObject(arg0).style;
return addHeapObject(ret); return addHeapObject(ret);
@ -789,6 +803,13 @@ async function init(input) {
var ret = new Uint8Array(getObject(arg0)); var ret = new Uint8Array(getObject(arg0));
return addHeapObject(ret); return addHeapObject(ret);
}; };
imports.wbg.__wbg_set_3bb960a9975f3cd2 = function(arg0, arg1, arg2) {
getObject(arg0).set(getObject(arg1), arg2 >>> 0);
};
imports.wbg.__wbg_length_2b13641a9d906653 = function(arg0) {
var ret = getObject(arg0).length;
return ret;
};
imports.wbg.__wbg_new_79f4487112eba5a7 = function(arg0) { imports.wbg.__wbg_new_79f4487112eba5a7 = function(arg0) {
var ret = new Float32Array(getObject(arg0)); var ret = new Float32Array(getObject(arg0));
return addHeapObject(ret); return addHeapObject(ret);
@ -849,36 +870,36 @@ async function init(input) {
var ret = wasm.memory; var ret = wasm.memory;
return addHeapObject(ret); return addHeapObject(ret);
}; };
imports.wbg.__wbindgen_closure_wrapper472 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper738 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 101, __wbg_adapter_26); var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_26);
return addHeapObject(ret); return addHeapObject(ret);
}; };
imports.wbg.__wbindgen_closure_wrapper473 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper739 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 101, __wbg_adapter_29); var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_29);
return addHeapObject(ret); return addHeapObject(ret);
}; };
imports.wbg.__wbindgen_closure_wrapper475 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper741 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 101, __wbg_adapter_32); var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_32);
return addHeapObject(ret); return addHeapObject(ret);
}; };
imports.wbg.__wbindgen_closure_wrapper477 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper743 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 101, __wbg_adapter_35); var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_35);
return addHeapObject(ret); return addHeapObject(ret);
}; };
imports.wbg.__wbindgen_closure_wrapper479 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper745 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 101, __wbg_adapter_38); var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_38);
return addHeapObject(ret); return addHeapObject(ret);
}; };
imports.wbg.__wbindgen_closure_wrapper482 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper748 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 101, __wbg_adapter_41); var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_41);
return addHeapObject(ret); return addHeapObject(ret);
}; };
imports.wbg.__wbindgen_closure_wrapper484 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper750 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 101, __wbg_adapter_44); var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_44);
return addHeapObject(ret); return addHeapObject(ret);
}; };
imports.wbg.__wbindgen_closure_wrapper1212 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper1475 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 132, __wbg_adapter_47); var ret = makeMutClosure(arg0, arg1, 242, __wbg_adapter_47);
return addHeapObject(ret); return addHeapObject(ret);
}; };

Binary file not shown.

View file

@ -77,12 +77,19 @@ pub struct AppOutput {
} }
pub trait TextureAllocator { pub trait TextureAllocator {
/// Allocate a user texture (EXPERIMENTAL!) /// A.locate a new user texture.
fn new_texture_srgba_premultiplied( fn alloc(&mut self) -> crate::TextureId;
/// Set or change the pixels of a user texture.
fn set_srgba_premultiplied(
&mut self, &mut self,
id: crate::TextureId,
size: (usize, usize), size: (usize, usize),
pixels: &[crate::Srgba], srgba_pixels: &[crate::Srgba],
) -> crate::TextureId; );
/// Free the given texture.
fn free(&mut self, id: crate::TextureId);
} }
/// A place where you can store custom data in a way that persists when you restart the app. /// A place where you can store custom data in a way that persists when you restart the app.

View file

@ -41,7 +41,7 @@ impl ColorTest {
ui.label("Use a color picker to ensure this color is (255, 165, 0) / #ffa500"); ui.label("Use a color picker to ensure this color is (255, 165, 0) / #ffa500");
ui.wrap(|ui| { ui.wrap(|ui| {
ui.style_mut().spacing.item_spacing.y = 0.0; // No spacing between gradients ui.style_mut().spacing.item_spacing.y = 0.0; // No spacing between gradients
let g = Gradient::one_color(Srgba::new(255, 165, 0, 255)); let g = Gradient::one_color(Srgba::from_rgb(255, 165, 0));
self.vertex_gradient(ui, "orange rgb(255, 165, 0) - vertex", WHITE, &g); self.vertex_gradient(ui, "orange rgb(255, 165, 0) - vertex", WHITE, &g);
self.tex_gradient( self.tex_gradient(
ui, ui,
@ -125,7 +125,7 @@ impl ColorTest {
ui, ui,
tex_allocator, tex_allocator,
RED, RED,
(TRANSPARENT, Srgba::new(0, 0, 255, 0)), (TRANSPARENT, Srgba::from_rgba_premultiplied(0, 0, 255, 0)),
); );
ui.separator(); ui.separator();
@ -367,7 +367,9 @@ impl TextureManager {
let pixels = gradient.to_pixel_row(); let pixels = gradient.to_pixel_row();
let width = pixels.len(); let width = pixels.len();
let height = 1; let height = 1;
tex_allocator.new_texture_srgba_premultiplied((width, height), &pixels) let id = tex_allocator.alloc();
tex_allocator.set_srgba_premultiplied(id, (width, height), &pixels);
id
}) })
} }
} }

View file

@ -24,15 +24,41 @@ impl std::ops::IndexMut<usize> for Srgba {
} }
} }
// TODO: remove ?
pub const fn srgba(r: u8, g: u8, b: u8, a: u8) -> Srgba { pub const fn srgba(r: u8, g: u8, b: u8, a: u8) -> Srgba {
Srgba::new(r, g, b, a) Srgba::from_rgba_premultiplied(r, g, b, a)
} }
impl Srgba { impl Srgba {
#[deprecated = "Use from_rgb(..), from_rgba_premultiplied(..) or from_srgba_unmultiplied(..)"]
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self([r, g, b, a]) Self([r, g, b, a])
} }
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
Self([r, g, b, 255])
}
pub const fn from_rgb_additive(r: u8, g: u8, b: u8) -> Self {
Self([r, g, b, 0])
}
/// From `sRGBA` with premultiplied alpha.
pub const fn from_rgba_premultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
Self([r, g, b, a])
}
/// From `sRGBA` WITHOUT premultiplied alpha.
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
if a == 255 {
Self::from_rgba_premultiplied(r, g, b, a) // common-case optimization
} else {
Rgba::from(Self::from_rgb(r, g, b))
.multiply(a as f32 / 255.0)
.into()
}
}
pub const fn gray(l: u8) -> Self { pub const fn gray(l: u8) -> Self {
Self([l, l, l, 255]) Self([l, l, l, 255])
} }
@ -476,7 +502,7 @@ fn test_hsv_roundtrip() {
for r in 0..=255 { for r in 0..=255 {
for g in 0..=255 { for g in 0..=255 {
for b in 0..=255 { for b in 0..=255 {
let srgba = Srgba::new(r, g, b, 255); let srgba = Srgba::from_rgb(r, g, b);
let hsva = Hsva::from(srgba); let hsva = Hsva::from(srgba);
assert_eq!(srgba, Srgba::from(hsva)); assert_eq!(srgba, Srgba::from(hsva));
} }

View file

@ -11,12 +11,21 @@ const EGUI_MEMORY_KEY: &str = "egui";
const WINDOW_KEY: &str = "window"; const WINDOW_KEY: &str = "window";
impl egui::app::TextureAllocator for Painter { impl egui::app::TextureAllocator for Painter {
fn new_texture_srgba_premultiplied( fn alloc(&mut self) -> egui::TextureId {
self.alloc_user_texture()
}
fn set_srgba_premultiplied(
&mut self, &mut self,
id: egui::TextureId,
size: (usize, usize), size: (usize, usize),
pixels: &[Srgba], srgba_pixels: &[Srgba],
) -> egui::TextureId { ) {
self.new_user_texture(size, pixels) self.set_user_texture(id, size, srgba_pixels);
}
fn free(&mut self, id: egui::TextureId) {
self.free_user_texture(id)
} }
} }

View file

@ -67,7 +67,8 @@ pub struct Painter {
egui_texture: Option<SrgbTexture2d>, egui_texture: Option<SrgbTexture2d>,
egui_texture_version: Option<u64>, egui_texture_version: Option<u64>,
user_textures: Vec<UserTexture>, /// `None` means unallocated (freed) slot.
user_textures: Vec<Option<UserTexture>>,
} }
#[derive(Default)] #[derive(Default)]
@ -77,7 +78,7 @@ struct UserTexture {
pixels: Vec<Vec<(u8, u8, u8, u8)>>, pixels: Vec<Vec<(u8, u8, u8, u8)>>,
/// Lazily uploaded /// Lazily uploaded
texture: Option<SrgbTexture2d>, gl_texture: Option<SrgbTexture2d>,
} }
impl Painter { impl Painter {
@ -94,20 +95,62 @@ impl Painter {
} }
} }
pub fn new_user_texture(&mut self, size: (usize, usize), pixels: &[Srgba]) -> egui::TextureId { pub fn alloc_user_texture(&mut self) -> egui::TextureId {
for (i, tex) in self.user_textures.iter_mut().enumerate() {
if tex.is_none() {
*tex = Some(Default::default());
return egui::TextureId::User(i as u64);
}
}
let id = egui::TextureId::User(self.user_textures.len() as u64);
self.user_textures.push(Some(Default::default()));
id
}
pub fn set_user_texture(
&mut self,
id: egui::TextureId,
size: (usize, usize),
pixels: &[Srgba],
) {
assert_eq!(size.0 * size.1, pixels.len()); assert_eq!(size.0 * size.1, pixels.len());
let pixels: Vec<Vec<(u8, u8, u8, u8)>> = pixels if let egui::TextureId::User(id) = id {
.chunks(size.0 as usize) if let Some(user_texture) = self.user_textures.get_mut(id as usize) {
.map(|row| row.iter().map(|srgba| srgba.to_tuple()).collect()) if let Some(user_texture) = user_texture {
.collect(); let pixels: Vec<Vec<(u8, u8, u8, u8)>> = pixels
.chunks(size.0 as usize)
.map(|row| row.iter().map(|srgba| srgba.to_tuple()).collect())
.collect();
let id = egui::TextureId::User(self.user_textures.len() as u64); *user_texture = UserTexture {
self.user_textures.push(UserTexture { pixels,
pixels, gl_texture: None,
texture: None, };
}); }
id }
}
}
pub fn free_user_texture(&mut self, id: egui::TextureId) {
if let egui::TextureId::User(id) = id {
let index = id as usize;
if index < self.user_textures.len() {
self.user_textures[index] = None;
}
}
}
fn get_texture(&self, texture_id: egui::TextureId) -> Option<&SrgbTexture2d> {
match texture_id {
egui::TextureId::Egui => self.egui_texture.as_ref(),
egui::TextureId::User(id) => self
.user_textures
.get(id as usize)?
.as_ref()?
.gl_texture
.as_ref(),
}
} }
fn upload_egui_texture( fn upload_egui_texture(
@ -138,12 +181,14 @@ impl Painter {
fn upload_pending_user_textures(&mut self, facade: &dyn glium::backend::Facade) { fn upload_pending_user_textures(&mut self, facade: &dyn glium::backend::Facade) {
for user_texture in &mut self.user_textures { for user_texture in &mut self.user_textures {
if user_texture.texture.is_none() { if let Some(user_texture) = user_texture {
let pixels = std::mem::take(&mut user_texture.pixels); if user_texture.gl_texture.is_none() {
let format = texture::SrgbFormat::U8U8U8U8; let pixels = std::mem::take(&mut user_texture.pixels);
let mipmaps = texture::MipmapsOption::NoMipmap; let format = texture::SrgbFormat::U8U8U8U8;
user_texture.texture = let mipmaps = texture::MipmapsOption::NoMipmap;
Some(SrgbTexture2d::with_format(facade, pixels, format, mipmaps).unwrap()); user_texture.gl_texture =
Some(SrgbTexture2d::with_format(facade, pixels, format, mipmaps).unwrap());
}
} }
} }
} }
@ -173,18 +218,6 @@ impl Painter {
target.finish().unwrap(); target.finish().unwrap();
} }
fn get_texture(&self, texture_id: egui::TextureId) -> &SrgbTexture2d {
match texture_id {
egui::TextureId::Egui => self.egui_texture.as_ref().unwrap(),
egui::TextureId::User(id) => {
let id = id as usize;
assert!(id < self.user_textures.len());
let texture = self.user_textures[id].texture.as_ref();
texture.expect("Should have been uploaded")
}
}
}
#[inline(never)] // Easier profiling #[inline(never)] // Easier profiling
fn paint_job( fn paint_job(
&mut self, &mut self,
@ -229,68 +262,68 @@ impl Painter {
let width_in_points = width_in_pixels as f32 / pixels_per_point; let width_in_points = width_in_pixels as f32 / pixels_per_point;
let height_in_points = height_in_pixels as f32 / pixels_per_point; let height_in_points = height_in_pixels as f32 / pixels_per_point;
let texture = self.get_texture(triangles.texture_id); if let Some(texture) = self.get_texture(triangles.texture_id) {
let uniforms = uniform! {
u_screen_size: [width_in_points, height_in_points],
u_sampler: texture.sampled().wrap_function(SamplerWrapFunction::Clamp),
};
let uniforms = uniform! { // Egui outputs colors with premultiplied alpha:
u_screen_size: [width_in_points, height_in_points], let color_blend_func = glium::BlendingFunction::Addition {
u_sampler: texture.sampled().wrap_function(SamplerWrapFunction::Clamp), source: glium::LinearBlendingFactor::One,
}; destination: glium::LinearBlendingFactor::OneMinusSourceAlpha,
};
// Egui outputs colors with premultiplied alpha: // Less important, but this is technically the correct alpha blend function
let color_blend_func = glium::BlendingFunction::Addition { // when you want to make use of the framebuffer alpha (for screenshots, compositing, etc).
source: glium::LinearBlendingFactor::One, let alpha_blend_func = glium::BlendingFunction::Addition {
destination: glium::LinearBlendingFactor::OneMinusSourceAlpha, source: glium::LinearBlendingFactor::OneMinusDestinationAlpha,
}; destination: glium::LinearBlendingFactor::One,
};
// Less important, but this is technically the correct alpha blend function let blend = glium::Blend {
// when you want to make use of the framebuffer alpha (for screenshots, compositing, etc). color: color_blend_func,
let alpha_blend_func = glium::BlendingFunction::Addition { alpha: alpha_blend_func,
source: glium::LinearBlendingFactor::OneMinusDestinationAlpha, ..Default::default()
destination: glium::LinearBlendingFactor::One, };
};
let blend = glium::Blend { // Transform clip rect to physical pixels:
color: color_blend_func, let clip_min_x = pixels_per_point * clip_rect.min.x;
alpha: alpha_blend_func, let clip_min_y = pixels_per_point * clip_rect.min.y;
..Default::default() let clip_max_x = pixels_per_point * clip_rect.max.x;
}; let clip_max_y = pixels_per_point * clip_rect.max.y;
// Transform clip rect to physical pixels: // Make sure clip rect can fit withing an `u32`:
let clip_min_x = pixels_per_point * clip_rect.min.x; let clip_min_x = clamp(clip_min_x, 0.0..=width_in_pixels as f32);
let clip_min_y = pixels_per_point * clip_rect.min.y; let clip_min_y = clamp(clip_min_y, 0.0..=height_in_pixels as f32);
let clip_max_x = pixels_per_point * clip_rect.max.x; let clip_max_x = clamp(clip_max_x, clip_min_x..=width_in_pixels as f32);
let clip_max_y = pixels_per_point * clip_rect.max.y; let clip_max_y = clamp(clip_max_y, clip_min_y..=height_in_pixels as f32);
// Make sure clip rect can fit withing an `u32`: let clip_min_x = clip_min_x.round() as u32;
let clip_min_x = clamp(clip_min_x, 0.0..=width_in_pixels as f32); let clip_min_y = clip_min_y.round() as u32;
let clip_min_y = clamp(clip_min_y, 0.0..=height_in_pixels as f32); let clip_max_x = clip_max_x.round() as u32;
let clip_max_x = clamp(clip_max_x, clip_min_x..=width_in_pixels as f32); let clip_max_y = clip_max_y.round() as u32;
let clip_max_y = clamp(clip_max_y, clip_min_y..=height_in_pixels as f32);
let clip_min_x = clip_min_x.round() as u32; let params = glium::DrawParameters {
let clip_min_y = clip_min_y.round() as u32; blend,
let clip_max_x = clip_max_x.round() as u32; scissor: Some(glium::Rect {
let clip_max_y = clip_max_y.round() as u32; left: clip_min_x,
bottom: height_in_pixels - clip_max_y,
width: clip_max_x - clip_min_x,
height: clip_max_y - clip_min_y,
}),
..Default::default()
};
let params = glium::DrawParameters { target
blend, .draw(
scissor: Some(glium::Rect { &vertex_buffer,
left: clip_min_x, &index_buffer,
bottom: height_in_pixels - clip_max_y, &self.program,
width: clip_max_x - clip_min_x, &uniforms,
height: clip_max_y - clip_min_y, &params,
}), )
..Default::default() .unwrap();
}; }
target
.draw(
&vertex_buffer,
&index_buffer,
&self.program,
&uniforms,
&params,
)
.unwrap();
} }
} }

View file

@ -81,12 +81,21 @@ impl WebBackend {
} }
impl egui::app::TextureAllocator for webgl::Painter { impl egui::app::TextureAllocator for webgl::Painter {
fn new_texture_srgba_premultiplied( fn alloc(&mut self) -> egui::TextureId {
self.alloc_user_texture()
}
fn set_srgba_premultiplied(
&mut self, &mut self,
id: egui::TextureId,
size: (usize, usize), size: (usize, usize),
pixels: &[Srgba], srgba_pixels: &[Srgba],
) -> egui::TextureId { ) {
self.new_user_texture(size, pixels) self.set_user_texture(id, size, srgba_pixels);
}
fn free(&mut self, id: egui::TextureId) {
self.free_user_texture(id)
} }
} }

View file

@ -6,48 +6,80 @@ pub struct Response {
pub status: u16, pub status: u16,
pub status_text: String, pub status_text: String,
pub body: String, /// Content-Type header, or empty string if missing
pub header_content_type: String,
/// The raw bytes
pub bytes: Vec<u8>,
/// UTF-8 decoded version of bytes.
/// ONLY if `header_content_type` starts with "text" and bytes is UTF-8.
pub text: Option<String>,
} }
/// NOTE: Ok(..) is returned on network error. /// NOTE: Ok(..) is returned on network error.
/// Err is only for failure to use the fetch api. /// Err is only for failure to use the fetch api.
pub async fn get_text(url: &str) -> Result<Response, String> { pub async fn fetch(method: &str, url: &str) -> Result<Response, String> {
get_text_jsvalue(url) fetch_jsvalue(method, url)
.await .await
.map_err(|err| err.as_string().unwrap_or_default()) .map_err(|err| err.as_string().unwrap_or_default())
} }
/// NOTE: Ok(..) is returned on network error. /// NOTE: Ok(..) is returned on network error.
/// Err is only for failure to use the fetch api. /// Err is only for failure to use the fetch api.
async fn get_text_jsvalue(url: &str) -> Result<Response, JsValue> { pub async fn get(url: &str) -> Result<Response, String> {
fetch("GET", url).await
}
/// NOTE: Ok(..) is returned on network error.
/// Err is only for failure to use the fetch api.
async fn fetch_jsvalue(method: &str, url: &str) -> Result<Response, JsValue> {
// https://rustwasm.github.io/wasm-bindgen/examples/fetch.html // https://rustwasm.github.io/wasm-bindgen/examples/fetch.html
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture; use wasm_bindgen_futures::JsFuture;
let mut opts = web_sys::RequestInit::new(); let mut opts = web_sys::RequestInit::new();
opts.method("GET"); opts.method(method);
opts.mode(web_sys::RequestMode::Cors); opts.mode(web_sys::RequestMode::Cors);
let request = web_sys::Request::new_with_str_and_init(&url, &opts)?; let request = web_sys::Request::new_with_str_and_init(&url, &opts)?;
request.headers().set("Accept", "*/*")?; request.headers().set("Accept", "*/*")?;
let window = web_sys::window().unwrap(); let window = web_sys::window().unwrap();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?; let response = JsFuture::from(window.fetch_with_request(&request)).await?;
assert!(response.is_instance_of::<web_sys::Response>());
let response: web_sys::Response = response.dyn_into().unwrap();
assert!(resp_value.is_instance_of::<web_sys::Response>()); // // TODO: support binary get
let resp: web_sys::Response = resp_value.dyn_into().unwrap();
// TODO: headers // let body = JsFuture::from(response.text()?).await?;
// TODO: support binary get // let body = body.as_string().unwrap_or_default();
let body = JsFuture::from(resp.text()?).await?;
let body = body.as_string().unwrap_or_default(); let array_buffer = JsFuture::from(response.array_buffer()?).await?;
let uint8_array = js_sys::Uint8Array::new(&array_buffer);
let bytes = uint8_array.to_vec();
let header_content_type = response
.headers()
.get("Content-Type")
.ok()
.flatten()
.unwrap_or_default();
let text = if header_content_type.starts_with("text") {
String::from_utf8(bytes.clone()).ok()
} else {
None
};
Ok(Response { Ok(Response {
status_text: resp.status_text(), status_text: response.status_text(),
url: resp.url(), url: response.url(),
ok: resp.ok(), ok: response.ok(),
status: resp.status(), status: response.status(),
body, header_content_type,
bytes,
text,
}) })
} }

View file

@ -19,6 +19,10 @@ pub fn console_log(s: impl Into<JsValue>) {
web_sys::console::log_1(&s.into()); web_sys::console::log_1(&s.into());
} }
pub fn console_warn(s: impl Into<JsValue>) {
web_sys::console::warn_1(&s.into());
}
pub fn console_error(s: impl Into<JsValue>) { pub fn console_error(s: impl Into<JsValue>) {
web_sys::console::error_1(&s.into()); web_sys::console::error_1(&s.into());
} }

View file

@ -93,7 +93,8 @@ pub struct Painter {
egui_texture: WebGlTexture, egui_texture: WebGlTexture,
egui_texture_version: Option<u64>, egui_texture_version: Option<u64>,
user_textures: Vec<UserTexture>, /// `None` means unallocated (freed) slot.
user_textures: Vec<Option<UserTexture>>,
} }
#[derive(Default)] #[derive(Default)]
@ -104,7 +105,7 @@ struct UserTexture {
pixels: Vec<u8>, pixels: Vec<u8>,
/// Lazily uploaded /// Lazily uploaded
texture: Option<WebGlTexture>, gl_texture: Option<WebGlTexture>,
} }
impl Painter { impl Painter {
@ -165,28 +166,66 @@ impl Painter {
&self.canvas_id &self.canvas_id
} }
pub fn new_user_texture( pub fn alloc_user_texture(&mut self) -> egui::TextureId {
for (i, tex) in self.user_textures.iter_mut().enumerate() {
if tex.is_none() {
*tex = Some(Default::default());
return egui::TextureId::User(i as u64);
}
}
let id = egui::TextureId::User(self.user_textures.len() as u64);
self.user_textures.push(Some(Default::default()));
id
}
pub fn set_user_texture(
&mut self, &mut self,
id: egui::TextureId,
size: (usize, usize), size: (usize, usize),
srgba_pixels: &[Srgba], srgba_pixels: &[Srgba],
) -> egui::TextureId { ) {
assert_eq!(size.0 * size.1, srgba_pixels.len()); assert_eq!(size.0 * size.1, srgba_pixels.len());
let mut pixels: Vec<u8> = Vec::with_capacity(srgba_pixels.len() * 4); if let egui::TextureId::User(id) = id {
for srgba in srgba_pixels { if let Some(user_texture) = self.user_textures.get_mut(id as usize) {
pixels.push(srgba.r()); if let Some(user_texture) = user_texture {
pixels.push(srgba.g()); let mut pixels: Vec<u8> = Vec::with_capacity(srgba_pixels.len() * 4);
pixels.push(srgba.b()); for srgba in srgba_pixels {
pixels.push(srgba.a()); pixels.push(srgba.r());
} pixels.push(srgba.g());
pixels.push(srgba.b());
pixels.push(srgba.a());
}
let id = egui::TextureId::User(self.user_textures.len() as u64); *user_texture = UserTexture {
self.user_textures.push(UserTexture { size,
size, pixels,
pixels, gl_texture: None,
texture: None, };
}); }
id }
}
}
pub fn free_user_texture(&mut self, id: egui::TextureId) {
if let egui::TextureId::User(id) = id {
let index = id as usize;
if index < self.user_textures.len() {
self.user_textures[index] = None;
}
}
}
fn get_texture(&self, texture_id: egui::TextureId) -> Option<&WebGlTexture> {
match texture_id {
egui::TextureId::Egui => Some(&self.egui_texture),
egui::TextureId::User(id) => self
.user_textures
.get(id as usize)?
.as_ref()?
.gl_texture
.as_ref(),
}
} }
fn upload_egui_texture(&mut self, texture: &Texture) { fn upload_egui_texture(&mut self, texture: &Texture) {
@ -233,50 +272,40 @@ impl Painter {
let gl = &self.gl; let gl = &self.gl;
for user_texture in &mut self.user_textures { for user_texture in &mut self.user_textures {
if user_texture.texture.is_none() { if let Some(user_texture) = user_texture {
let pixels = std::mem::take(&mut user_texture.pixels); if user_texture.gl_texture.is_none() {
let pixels = std::mem::take(&mut user_texture.pixels);
let gl_texture = gl.create_texture().unwrap(); let gl_texture = gl.create_texture().unwrap();
gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture)); gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture));
gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_S, Gl::CLAMP_TO_EDGE as i32); gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_S, Gl::CLAMP_TO_EDGE as i32);
gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_T, Gl::CLAMP_TO_EDGE as i32); gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_T, Gl::CLAMP_TO_EDGE as i32);
gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MIN_FILTER, Gl::LINEAR as i32); gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MIN_FILTER, Gl::LINEAR as i32);
gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MAG_FILTER, Gl::LINEAR as i32); gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MAG_FILTER, Gl::LINEAR as i32);
gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture)); gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture));
// TODO: https://developer.mozilla.org/en-US/docs/Web/API/EXT_sRGB // TODO: https://developer.mozilla.org/en-US/docs/Web/API/EXT_sRGB
let level = 0; let level = 0;
let internal_format = Gl::RGBA; let internal_format = Gl::RGBA;
let border = 0; let border = 0;
let src_format = Gl::RGBA; let src_format = Gl::RGBA;
let src_type = Gl::UNSIGNED_BYTE; let src_type = Gl::UNSIGNED_BYTE;
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
Gl::TEXTURE_2D, Gl::TEXTURE_2D,
level, level,
internal_format as i32, internal_format as i32,
user_texture.size.0 as i32, user_texture.size.0 as i32,
user_texture.size.1 as i32, user_texture.size.1 as i32,
border, border,
src_format, src_format,
src_type, src_type,
Some(&pixels), Some(&pixels),
) )
.unwrap(); .unwrap();
user_texture.texture = Some(gl_texture); user_texture.gl_texture = Some(gl_texture);
} }
}
}
fn get_texture(&self, texture_id: egui::TextureId) -> &WebGlTexture {
match texture_id {
egui::TextureId::Egui => &self.egui_texture,
egui::TextureId::User(id) => {
let id = id as usize;
assert!(id < self.user_textures.len());
let texture = self.user_textures[id].texture.as_ref();
texture.expect("Should have been uploaded")
} }
} }
} }
@ -330,31 +359,38 @@ impl Painter {
gl.clear(Gl::COLOR_BUFFER_BIT); gl.clear(Gl::COLOR_BUFFER_BIT);
for (clip_rect, triangles) in jobs { for (clip_rect, triangles) in jobs {
gl.bind_texture(Gl::TEXTURE_2D, Some(self.get_texture(triangles.texture_id))); if let Some(gl_texture) = self.get_texture(triangles.texture_id) {
gl.bind_texture(Gl::TEXTURE_2D, Some(gl_texture));
let clip_min_x = pixels_per_point * clip_rect.min.x; let clip_min_x = pixels_per_point * clip_rect.min.x;
let clip_min_y = pixels_per_point * clip_rect.min.y; let clip_min_y = pixels_per_point * clip_rect.min.y;
let clip_max_x = pixels_per_point * clip_rect.max.x; let clip_max_x = pixels_per_point * clip_rect.max.x;
let clip_max_y = pixels_per_point * clip_rect.max.y; let clip_max_y = pixels_per_point * clip_rect.max.y;
let clip_min_x = clamp(clip_min_x, 0.0..=screen_size_pixels.x); let clip_min_x = clamp(clip_min_x, 0.0..=screen_size_pixels.x);
let clip_min_y = clamp(clip_min_y, 0.0..=screen_size_pixels.y); let clip_min_y = clamp(clip_min_y, 0.0..=screen_size_pixels.y);
let clip_max_x = clamp(clip_max_x, clip_min_x..=screen_size_pixels.x); let clip_max_x = clamp(clip_max_x, clip_min_x..=screen_size_pixels.x);
let clip_max_y = clamp(clip_max_y, clip_min_y..=screen_size_pixels.y); let clip_max_y = clamp(clip_max_y, clip_min_y..=screen_size_pixels.y);
let clip_min_x = clip_min_x.round() as i32; let clip_min_x = clip_min_x.round() as i32;
let clip_min_y = clip_min_y.round() as i32; let clip_min_y = clip_min_y.round() as i32;
let clip_max_x = clip_max_x.round() as i32; let clip_max_x = clip_max_x.round() as i32;
let clip_max_y = clip_max_y.round() as i32; let clip_max_y = clip_max_y.round() as i32;
// scissor Y coordinate is from the bottom // scissor Y coordinate is from the bottom
gl.scissor( gl.scissor(
clip_min_x, clip_min_x,
self.canvas.height() as i32 - clip_max_y, self.canvas.height() as i32 - clip_max_y,
clip_max_x - clip_min_x, clip_max_x - clip_min_x,
clip_max_y - clip_min_y, clip_max_y - clip_min_y,
); );
for triangles in triangles.split_to_u16() { for triangles in triangles.split_to_u16() {
self.paint_triangles(&triangles)?; self.paint_triangles(&triangles)?;
}
} else {
crate::console_warn(format!(
"WebGL: Failed to find texture {:?}",
triangles.texture_id
));
} }
} }
Ok(()) Ok(())

View file

@ -11,6 +11,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
egui = { path = "../egui", features = ["serde"] } egui = { path = "../egui", features = ["serde"] }
egui_web = { path = "../egui_web" } egui_web = { path = "../egui_web" }
image = { version = "0.23", default_features=false, features=["jpeg", "png"] }
js-sys = "0.3" js-sys = "0.3"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"

View file

@ -1,18 +1,50 @@
use egui_web::fetch::Response; use egui_web::fetch::Response;
use std::sync::mpsc::Receiver; use std::sync::mpsc::Receiver;
struct Image {
size: (usize, usize),
pixels: Vec<egui::Srgba>,
}
impl Image {
fn decode(bytes: &[u8]) -> Option<Image> {
use image::GenericImageView;
let image = image::load_from_memory(&bytes).ok()?;
let image_buffer = image.to_rgba();
let size = (image.width() as usize, image.height() as usize);
let pixels = image_buffer.into_vec();
assert_eq!(size.0 * size.1 * 4, pixels.len());
let pixels = pixels
.chunks(4)
.map(|p| egui::Srgba::from_rgba_unmultiplied(p[0], p[1], p[2], p[3]))
.collect();
Some(Image { size, pixels })
}
}
struct Resource {
/// HTTP response
response: Response,
/// If set, the response was an image.
image: Option<Image>,
}
pub struct ExampleApp { pub struct ExampleApp {
url: String, url: String,
receivers: Vec<Receiver<Result<Response, String>>>, in_progress: Option<Receiver<Result<Response, String>>>,
fetch_result: Option<Result<Response, String>>, result: Option<Result<Resource, String>>,
texture_id: Option<egui::TextureId>,
} }
impl Default for ExampleApp { impl Default for ExampleApp {
fn default() -> Self { fn default() -> Self {
Self { Self {
url: "https://raw.githubusercontent.com/emilk/egui/master/README.md".to_owned(), url: "https://raw.githubusercontent.com/emilk/egui/master/README.md".to_owned(),
receivers: Default::default(), in_progress: Default::default(),
fetch_result: Default::default(), result: Default::default(),
texture_id: None,
} }
} }
} }
@ -23,7 +55,7 @@ impl egui::app::App for ExampleApp {
fn ui( fn ui(
&mut self, &mut self,
ctx: &std::sync::Arc<egui::Context>, ctx: &std::sync::Arc<egui::Context>,
_integration_context: &mut egui::app::IntegrationContext, integration_context: &mut egui::app::IntegrationContext,
) { ) {
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("HTTP Get inside of Egui"); ui.heading("HTTP Get inside of Egui");
@ -32,49 +64,24 @@ impl egui::app::App for ExampleApp {
"(source code)" "(source code)"
)); ));
{ if ui_url(ui, &mut self.url) {
let mut trigger_fetch = false; let (sender, receiver) = std::sync::mpsc::channel();
self.in_progress = Some(receiver);
ui.horizontal(|ui| { let url = self.url.clone();
ui.label("URL:"); egui_web::spawn_future(async move {
trigger_fetch |= ui.text_edit_singleline(&mut self.url).lost_kb_focus; sender.send(egui_web::fetch::get(&url).await).ok();
// TODO: trigger egui repaint somehow
if ui.button("Egui README.md").clicked {
self.url = "https://raw.githubusercontent.com/emilk/egui/master/README.md"
.to_owned();
trigger_fetch = true;
}
if ui.button("Source code for this file").clicked {
self.url = format!(
"https://raw.githubusercontent.com/emilk/egui/master/{}",
file!()
);
trigger_fetch = true;
}
}); });
trigger_fetch |= ui.button("GET").clicked;
if trigger_fetch {
let (sender, receiver) = std::sync::mpsc::channel();
self.receivers.push(receiver);
let url = self.url.clone();
let future = async move {
let result = egui_web::fetch::get_text(&url).await;
sender.send(result).ok();
// TODO: trigger egui repaint somehow
};
egui_web::spawn_future(future);
}
} }
// Show finished download (if any): ui.separator();
if let Some(result) = &self.fetch_result {
ui.separator(); if self.in_progress.is_some() {
ui.label("Please wait...");
} else if let Some(result) = &self.result {
match result { match result {
Ok(response) => { Ok(resource) => {
ui_response(ui, response); ui_resouce(ui, self.texture_id, resource);
} }
Err(error) => { Err(error) => {
// This should only happen if the fetch API isn't available or something similar. // This should only happen if the fetch API isn't available or something similar.
@ -84,25 +91,100 @@ impl egui::app::App for ExampleApp {
} }
}); });
for i in (0..self.receivers.len()).rev() { self.poll_receiver(integration_context);
if let Ok(result) = self.receivers[i].try_recv() { }
self.fetch_result = Some(result); }
let _ = self.receivers.swap_remove(i);
impl ExampleApp {
fn load_image(
&mut self,
integration_context: &mut egui::app::IntegrationContext,
response: &Response,
) -> Option<Image> {
let tex_allocator = integration_context.tex_allocator.as_mut()?;
if matches!(
response.header_content_type.as_str(),
"image/jpeg" | "image/png"
) {
let image = Image::decode(&response.bytes)?;
let texture_id = self.texture_id.unwrap_or_else(|| tex_allocator.alloc());
self.texture_id = Some(texture_id);
tex_allocator.set_srgba_premultiplied(texture_id, image.size, &image.pixels);
return Some(image);
}
None
}
fn poll_receiver(&mut self, integration_context: &mut egui::app::IntegrationContext) {
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 {
image: self.load_image(integration_context, &response),
response,
}));
} }
} }
} }
} }
fn ui_response(ui: &mut egui::Ui, response: &Response) { fn ui_url(ui: &mut egui::Ui, url: &mut String) -> bool {
ui.monospace(format!("url: {}", response.url)); let mut trigger_fetch = false;
ui.horizontal(|ui| {
ui.label("URL:");
trigger_fetch |= ui.text_edit_singleline(url).lost_kb_focus;
if ui.button("Source code for this example").clicked {
*url = format!(
"https://raw.githubusercontent.com/emilk/egui/master/{}",
file!()
);
trigger_fetch = true;
}
if ui.button("Random image").clicked {
let seed = ui.input().time;
let width = 640;
let height = 480;
*url = format!("https://picsum.photos/seed/{}/{}/{}", seed, width, height);
trigger_fetch = true;
}
});
trigger_fetch |= ui.button("GET").clicked;
trigger_fetch
}
fn ui_resouce(ui: &mut egui::Ui, texture_id: Option<egui::TextureId>, resource: &Resource) {
let Resource { response, image } = resource;
ui.monospace(format!("url: {}", response.url));
ui.monospace(format!( ui.monospace(format!(
"status: {} ({})", "status: {} ({})",
response.status, response.status_text response.status, response.status_text
)); ));
ui.monospace(format!("Content-Type: {}", response.header_content_type));
ui.monospace(format!(
"Size: {:.1} kB",
response.bytes.len() as f32 / 1000.0
));
ui.monospace("Body:"); if let Some(image) = image {
ui.separator(); if let Some(texture_id) = texture_id {
egui::ScrollArea::auto_sized().show(ui, |ui| { ui.image(
ui.monospace(&response.body); texture_id,
}); egui::Vec2::new(image.size.0 as f32, image.size.1 as f32),
);
}
} else if let Some(text) = &response.text {
ui.monospace("Body:");
ui.separator();
egui::ScrollArea::auto_sized().show(ui, |ui| {
ui.monospace(text);
});
} else {
ui.monospace("[binary]");
}
} }