[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"
checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
[[package]]
name = "adler32"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
name = "ahash"
version = "0.4.6"
@ -72,7 +78,7 @@ dependencies = [
"addr2line",
"cfg-if 1.0.0",
"libc",
"miniz_oxide",
"miniz_oxide 0.4.3",
"object",
"rustc-demangle",
]
@ -107,6 +113,12 @@ version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820"
[[package]]
name = "bytemuck"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41aa2ec95ca3b5c54cf73c91acf06d24f4495d5f1b1c12506ae3483d646177ac"
[[package]]
name = "byteorder"
version = "1.3.4"
@ -245,6 +257,12 @@ dependencies = [
"objc",
]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "const_fn"
version = "0.4.3"
@ -333,6 +351,15 @@ dependencies = [
"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]]
name = "criterion"
version = "0.3.3"
@ -473,6 +500,16 @@ dependencies = [
"syn",
]
[[package]]
name = "deflate"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174"
dependencies = [
"adler32",
"byteorder",
]
[[package]]
name = "demo_glium"
version = "0.1.0"
@ -586,6 +623,7 @@ version = "0.1.0"
dependencies = [
"egui",
"egui_web",
"image",
"js-sys",
"serde",
"serde_json",
@ -755,6 +793,22 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "instant"
version = "0.1.8"
@ -794,6 +848,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "js-sys"
version = "0.3.45"
@ -905,6 +968,15 @@ dependencies = [
"autocfg",
]
[[package]]
name = "miniz_oxide"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
dependencies = [
"adler32",
]
[[package]]
name = "miniz_oxide"
version = "0.4.3"
@ -1046,6 +1118,28 @@ dependencies = [
"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]]
name = "num-traits"
version = "0.2.14"
@ -1202,6 +1296,18 @@ dependencies = [
"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]]
name = "proc-macro-crate"
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
* Egui-web fetch
* Egui-web local storage
## Other

View file

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

View file

@ -417,6 +417,13 @@ async function init(input) {
var ret = getObject(arg0).writeText(getStringFromWasm0(arg1, arg2));
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) {
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 + 0] = ptr0;
};
imports.wbg.__wbg_text_966d07536ca6ccdc = handleError(function(arg0) {
var ret = getObject(arg0).text();
imports.wbg.__wbg_headers_c736e1fe38752cff = function(arg0) {
var ret = getObject(arg0).headers;
return addHeapObject(ret);
};
imports.wbg.__wbg_arrayBuffer_dc33ab7b8cdf0d63 = handleError(function(arg0) {
var ret = getObject(arg0).arrayBuffer();
return addHeapObject(ret);
});
imports.wbg.__wbg_now_49847177a6d1d57e = function(arg0) {
@ -672,6 +683,9 @@ async function init(input) {
imports.wbg.__wbg_log_3bafd82835c6de6d = function(arg0) {
console.log(getObject(arg0));
};
imports.wbg.__wbg_warn_d05e82888b7fad05 = function(arg0) {
console.warn(getObject(arg0));
};
imports.wbg.__wbg_style_9a41d46c005f7596 = function(arg0) {
var ret = getObject(arg0).style;
return addHeapObject(ret);
@ -789,6 +803,13 @@ async function init(input) {
var ret = new Uint8Array(getObject(arg0));
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) {
var ret = new Float32Array(getObject(arg0));
return addHeapObject(ret);
@ -849,36 +870,36 @@ async function init(input) {
var ret = wasm.memory;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper472 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 101, __wbg_adapter_26);
imports.wbg.__wbindgen_closure_wrapper738 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_26);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper473 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 101, __wbg_adapter_29);
imports.wbg.__wbindgen_closure_wrapper739 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_29);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper475 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 101, __wbg_adapter_32);
imports.wbg.__wbindgen_closure_wrapper741 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_32);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper477 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 101, __wbg_adapter_35);
imports.wbg.__wbindgen_closure_wrapper743 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_35);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper479 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 101, __wbg_adapter_38);
imports.wbg.__wbindgen_closure_wrapper745 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_38);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper482 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 101, __wbg_adapter_41);
imports.wbg.__wbindgen_closure_wrapper748 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_41);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper484 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 101, __wbg_adapter_44);
imports.wbg.__wbindgen_closure_wrapper750 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 211, __wbg_adapter_44);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper1212 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 132, __wbg_adapter_47);
imports.wbg.__wbindgen_closure_wrapper1475 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 242, __wbg_adapter_47);
return addHeapObject(ret);
};

Binary file not shown.

View file

@ -77,12 +77,19 @@ pub struct AppOutput {
}
pub trait TextureAllocator {
/// Allocate a user texture (EXPERIMENTAL!)
fn new_texture_srgba_premultiplied(
/// A.locate a new user texture.
fn alloc(&mut self) -> crate::TextureId;
/// Set or change the pixels of a user texture.
fn set_srgba_premultiplied(
&mut self,
id: crate::TextureId,
size: (usize, usize),
pixels: &[crate::Srgba],
) -> crate::TextureId;
srgba_pixels: &[crate::Srgba],
);
/// 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.

View file

@ -41,7 +41,7 @@ impl ColorTest {
ui.label("Use a color picker to ensure this color is (255, 165, 0) / #ffa500");
ui.wrap(|ui| {
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.tex_gradient(
ui,
@ -125,7 +125,7 @@ impl ColorTest {
ui,
tex_allocator,
RED,
(TRANSPARENT, Srgba::new(0, 0, 255, 0)),
(TRANSPARENT, Srgba::from_rgba_premultiplied(0, 0, 255, 0)),
);
ui.separator();
@ -367,7 +367,9 @@ impl TextureManager {
let pixels = gradient.to_pixel_row();
let width = pixels.len();
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 {
Srgba::new(r, g, b, a)
Srgba::from_rgba_premultiplied(r, g, b, a)
}
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 {
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 {
Self([l, l, l, 255])
}
@ -476,7 +502,7 @@ fn test_hsv_roundtrip() {
for r in 0..=255 {
for g 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);
assert_eq!(srgba, Srgba::from(hsva));
}

View file

@ -11,12 +11,21 @@ const EGUI_MEMORY_KEY: &str = "egui";
const WINDOW_KEY: &str = "window";
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,
id: egui::TextureId,
size: (usize, usize),
pixels: &[Srgba],
) -> egui::TextureId {
self.new_user_texture(size, pixels)
srgba_pixels: &[Srgba],
) {
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_version: Option<u64>,
user_textures: Vec<UserTexture>,
/// `None` means unallocated (freed) slot.
user_textures: Vec<Option<UserTexture>>,
}
#[derive(Default)]
@ -77,7 +78,7 @@ struct UserTexture {
pixels: Vec<Vec<(u8, u8, u8, u8)>>,
/// Lazily uploaded
texture: Option<SrgbTexture2d>,
gl_texture: Option<SrgbTexture2d>,
}
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());
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();
if let egui::TextureId::User(id) = id {
if let Some(user_texture) = self.user_textures.get_mut(id as usize) {
if let Some(user_texture) = user_texture {
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);
self.user_textures.push(UserTexture {
pixels,
texture: None,
});
id
*user_texture = UserTexture {
pixels,
gl_texture: None,
};
}
}
}
}
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(
@ -138,12 +181,14 @@ impl Painter {
fn upload_pending_user_textures(&mut self, facade: &dyn glium::backend::Facade) {
for user_texture in &mut self.user_textures {
if user_texture.texture.is_none() {
let pixels = std::mem::take(&mut user_texture.pixels);
let format = texture::SrgbFormat::U8U8U8U8;
let mipmaps = texture::MipmapsOption::NoMipmap;
user_texture.texture =
Some(SrgbTexture2d::with_format(facade, pixels, format, mipmaps).unwrap());
if let Some(user_texture) = user_texture {
if user_texture.gl_texture.is_none() {
let pixels = std::mem::take(&mut user_texture.pixels);
let format = texture::SrgbFormat::U8U8U8U8;
let mipmaps = texture::MipmapsOption::NoMipmap;
user_texture.gl_texture =
Some(SrgbTexture2d::with_format(facade, pixels, format, mipmaps).unwrap());
}
}
}
}
@ -173,18 +218,6 @@ impl Painter {
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
fn paint_job(
&mut self,
@ -229,68 +262,68 @@ impl Painter {
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 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! {
u_screen_size: [width_in_points, height_in_points],
u_sampler: texture.sampled().wrap_function(SamplerWrapFunction::Clamp),
};
// Egui outputs colors with premultiplied alpha:
let color_blend_func = glium::BlendingFunction::Addition {
source: glium::LinearBlendingFactor::One,
destination: glium::LinearBlendingFactor::OneMinusSourceAlpha,
};
// Egui outputs colors with premultiplied alpha:
let color_blend_func = glium::BlendingFunction::Addition {
source: glium::LinearBlendingFactor::One,
destination: glium::LinearBlendingFactor::OneMinusSourceAlpha,
};
// Less important, but this is technically the correct alpha blend function
// when you want to make use of the framebuffer alpha (for screenshots, compositing, etc).
let alpha_blend_func = glium::BlendingFunction::Addition {
source: glium::LinearBlendingFactor::OneMinusDestinationAlpha,
destination: glium::LinearBlendingFactor::One,
};
// Less important, but this is technically the correct alpha blend function
// when you want to make use of the framebuffer alpha (for screenshots, compositing, etc).
let alpha_blend_func = glium::BlendingFunction::Addition {
source: glium::LinearBlendingFactor::OneMinusDestinationAlpha,
destination: glium::LinearBlendingFactor::One,
};
let blend = glium::Blend {
color: color_blend_func,
alpha: alpha_blend_func,
..Default::default()
};
let blend = glium::Blend {
color: color_blend_func,
alpha: alpha_blend_func,
..Default::default()
};
// Transform clip rect to physical pixels:
let clip_min_x = pixels_per_point * clip_rect.min.x;
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_y = pixels_per_point * clip_rect.max.y;
// Transform clip rect to physical pixels:
let clip_min_x = pixels_per_point * clip_rect.min.x;
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_y = pixels_per_point * clip_rect.max.y;
// Make sure clip rect can fit withing an `u32`:
let clip_min_x = clamp(clip_min_x, 0.0..=width_in_pixels as f32);
let clip_min_y = clamp(clip_min_y, 0.0..=height_in_pixels as f32);
let clip_max_x = clamp(clip_max_x, clip_min_x..=width_in_pixels as f32);
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 = clamp(clip_min_x, 0.0..=width_in_pixels as f32);
let clip_min_y = clamp(clip_min_y, 0.0..=height_in_pixels as f32);
let clip_max_x = clamp(clip_max_x, clip_min_x..=width_in_pixels as f32);
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 clip_min_y = clip_min_y.round() as u32;
let clip_max_x = clip_max_x.round() as u32;
let clip_max_y = clip_max_y.round() as u32;
let clip_min_x = clip_min_x.round() as u32;
let clip_min_y = clip_min_y.round() as u32;
let clip_max_x = clip_max_x.round() as u32;
let clip_max_y = clip_max_y.round() as u32;
let params = glium::DrawParameters {
blend,
scissor: Some(glium::Rect {
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 {
blend,
scissor: Some(glium::Rect {
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()
};
target
.draw(
&vertex_buffer,
&index_buffer,
&self.program,
&uniforms,
&params,
)
.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 {
fn new_texture_srgba_premultiplied(
fn alloc(&mut self) -> egui::TextureId {
self.alloc_user_texture()
}
fn set_srgba_premultiplied(
&mut self,
id: egui::TextureId,
size: (usize, usize),
pixels: &[Srgba],
) -> egui::TextureId {
self.new_user_texture(size, pixels)
srgba_pixels: &[Srgba],
) {
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_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.
/// Err is only for failure to use the fetch api.
pub async fn get_text(url: &str) -> Result<Response, String> {
get_text_jsvalue(url)
pub async fn fetch(method: &str, url: &str) -> Result<Response, String> {
fetch_jsvalue(method, url)
.await
.map_err(|err| err.as_string().unwrap_or_default())
}
/// NOTE: Ok(..) is returned on network error.
/// 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
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
let mut opts = web_sys::RequestInit::new();
opts.method("GET");
opts.method(method);
opts.mode(web_sys::RequestMode::Cors);
let request = web_sys::Request::new_with_str_and_init(&url, &opts)?;
request.headers().set("Accept", "*/*")?;
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>());
let resp: web_sys::Response = resp_value.dyn_into().unwrap();
// // TODO: support binary get
// TODO: headers
// TODO: support binary get
let body = JsFuture::from(resp.text()?).await?;
let body = body.as_string().unwrap_or_default();
// let body = JsFuture::from(response.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 {
status_text: resp.status_text(),
url: resp.url(),
ok: resp.ok(),
status: resp.status(),
body,
status_text: response.status_text(),
url: response.url(),
ok: response.ok(),
status: response.status(),
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());
}
pub fn console_warn(s: impl Into<JsValue>) {
web_sys::console::warn_1(&s.into());
}
pub fn console_error(s: impl Into<JsValue>) {
web_sys::console::error_1(&s.into());
}

View file

@ -93,7 +93,8 @@ pub struct Painter {
egui_texture: WebGlTexture,
egui_texture_version: Option<u64>,
user_textures: Vec<UserTexture>,
/// `None` means unallocated (freed) slot.
user_textures: Vec<Option<UserTexture>>,
}
#[derive(Default)]
@ -104,7 +105,7 @@ struct UserTexture {
pixels: Vec<u8>,
/// Lazily uploaded
texture: Option<WebGlTexture>,
gl_texture: Option<WebGlTexture>,
}
impl Painter {
@ -165,28 +166,66 @@ impl Painter {
&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,
id: egui::TextureId,
size: (usize, usize),
srgba_pixels: &[Srgba],
) -> egui::TextureId {
) {
assert_eq!(size.0 * size.1, srgba_pixels.len());
let mut pixels: Vec<u8> = Vec::with_capacity(srgba_pixels.len() * 4);
for srgba in srgba_pixels {
pixels.push(srgba.r());
pixels.push(srgba.g());
pixels.push(srgba.b());
pixels.push(srgba.a());
}
if let egui::TextureId::User(id) = id {
if let Some(user_texture) = self.user_textures.get_mut(id as usize) {
if let Some(user_texture) = user_texture {
let mut pixels: Vec<u8> = Vec::with_capacity(srgba_pixels.len() * 4);
for srgba in srgba_pixels {
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);
self.user_textures.push(UserTexture {
size,
pixels,
texture: None,
});
id
*user_texture = UserTexture {
size,
pixels,
gl_texture: None,
};
}
}
}
}
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) {
@ -233,50 +272,40 @@ impl Painter {
let gl = &self.gl;
for user_texture in &mut self.user_textures {
if user_texture.texture.is_none() {
let pixels = std::mem::take(&mut user_texture.pixels);
if let Some(user_texture) = user_texture {
if user_texture.gl_texture.is_none() {
let pixels = std::mem::take(&mut user_texture.pixels);
let gl_texture = gl.create_texture().unwrap();
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_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_MAG_FILTER, Gl::LINEAR as i32);
let gl_texture = gl.create_texture().unwrap();
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_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_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
let level = 0;
let internal_format = Gl::RGBA;
let border = 0;
let src_format = Gl::RGBA;
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::TEXTURE_2D,
level,
internal_format as i32,
user_texture.size.0 as i32,
user_texture.size.1 as i32,
border,
src_format,
src_type,
Some(&pixels),
)
.unwrap();
// TODO: https://developer.mozilla.org/en-US/docs/Web/API/EXT_sRGB
let level = 0;
let internal_format = Gl::RGBA;
let border = 0;
let src_format = Gl::RGBA;
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::TEXTURE_2D,
level,
internal_format as i32,
user_texture.size.0 as i32,
user_texture.size.1 as i32,
border,
src_format,
src_type,
Some(&pixels),
)
.unwrap();
user_texture.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")
user_texture.gl_texture = Some(gl_texture);
}
}
}
}
@ -330,31 +359,38 @@ impl Painter {
gl.clear(Gl::COLOR_BUFFER_BIT);
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_y = pixels_per_point * clip_rect.min.y;
let clip_max_x = pixels_per_point * clip_rect.max.x;
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_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_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_y = clip_min_y.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_min_x = pixels_per_point * clip_rect.min.x;
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_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_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_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_y = clip_min_y.round() as i32;
let clip_max_x = clip_max_x.round() as i32;
let clip_max_y = clip_max_y.round() as i32;
// scissor Y coordinate is from the bottom
gl.scissor(
clip_min_x,
self.canvas.height() as i32 - clip_max_y,
clip_max_x - clip_min_x,
clip_max_y - clip_min_y,
);
// scissor Y coordinate is from the bottom
gl.scissor(
clip_min_x,
self.canvas.height() as i32 - clip_max_y,
clip_max_x - clip_min_x,
clip_max_y - clip_min_y,
);
for triangles in triangles.split_to_u16() {
self.paint_triangles(&triangles)?;
for triangles in triangles.split_to_u16() {
self.paint_triangles(&triangles)?;
}
} else {
crate::console_warn(format!(
"WebGL: Failed to find texture {:?}",
triangles.texture_id
));
}
}
Ok(())

View file

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

View file

@ -1,18 +1,50 @@
use egui_web::fetch::Response;
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 {
url: String,
receivers: Vec<Receiver<Result<Response, String>>>,
fetch_result: Option<Result<Response, String>>,
in_progress: Option<Receiver<Result<Response, String>>>,
result: Option<Result<Resource, String>>,
texture_id: Option<egui::TextureId>,
}
impl Default for ExampleApp {
fn default() -> Self {
Self {
url: "https://raw.githubusercontent.com/emilk/egui/master/README.md".to_owned(),
receivers: Default::default(),
fetch_result: Default::default(),
in_progress: Default::default(),
result: Default::default(),
texture_id: None,
}
}
}
@ -23,7 +55,7 @@ impl egui::app::App for ExampleApp {
fn ui(
&mut self,
ctx: &std::sync::Arc<egui::Context>,
_integration_context: &mut egui::app::IntegrationContext,
integration_context: &mut egui::app::IntegrationContext,
) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("HTTP Get inside of Egui");
@ -32,49 +64,24 @@ impl egui::app::App for ExampleApp {
"(source code)"
));
{
let mut trigger_fetch = false;
ui.horizontal(|ui| {
ui.label("URL:");
trigger_fetch |= ui.text_edit_singleline(&mut self.url).lost_kb_focus;
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;
}
if ui_url(ui, &mut self.url) {
let (sender, receiver) = std::sync::mpsc::channel();
self.in_progress = Some(receiver);
let url = self.url.clone();
egui_web::spawn_future(async move {
sender.send(egui_web::fetch::get(&url).await).ok();
// TODO: trigger egui repaint somehow
});
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):
if let Some(result) = &self.fetch_result {
ui.separator();
ui.separator();
if self.in_progress.is_some() {
ui.label("Please wait...");
} else if let Some(result) = &self.result {
match result {
Ok(response) => {
ui_response(ui, response);
Ok(resource) => {
ui_resouce(ui, self.texture_id, resource);
}
Err(error) => {
// 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() {
if let Ok(result) = self.receivers[i].try_recv() {
self.fetch_result = Some(result);
let _ = self.receivers.swap_remove(i);
self.poll_receiver(integration_context);
}
}
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) {
ui.monospace(format!("url: {}", response.url));
fn ui_url(ui: &mut egui::Ui, url: &mut String) -> bool {
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!(
"status: {} ({})",
"status: {} ({})",
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:");
ui.separator();
egui::ScrollArea::auto_sized().show(ui, |ui| {
ui.monospace(&response.body);
});
if let Some(image) = image {
if let Some(texture_id) = texture_id {
ui.image(
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]");
}
}