[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:
parent
90cecace0c
commit
c6ce0b9e8c
16 changed files with 645 additions and 277 deletions
108
Cargo.lock
generated
108
Cargo.lock
generated
|
@ -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"
|
||||
|
|
2
TODO.md
2
TODO.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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: */
|
||||
|
|
|
@ -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.
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
||||
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 {
|
||||
*user_texture = UserTexture {
|
||||
pixels,
|
||||
texture: None,
|
||||
});
|
||||
id
|
||||
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,15 +181,17 @@ 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() {
|
||||
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.texture =
|
||||
user_texture.gl_texture =
|
||||
Some(SrgbTexture2d::with_format(facade, pixels, format, mipmaps).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Main entry-point for painting a frame
|
||||
pub fn paint_jobs(
|
||||
|
@ -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,8 +262,7 @@ 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),
|
||||
|
@ -293,4 +325,5 @@ impl Painter {
|
|||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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,13 +166,29 @@ 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());
|
||||
|
||||
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());
|
||||
|
@ -180,13 +197,35 @@ impl Painter {
|
|||
pixels.push(srgba.a());
|
||||
}
|
||||
|
||||
let id = egui::TextureId::User(self.user_textures.len() as u64);
|
||||
self.user_textures.push(UserTexture {
|
||||
*user_texture = UserTexture {
|
||||
size,
|
||||
pixels,
|
||||
texture: None,
|
||||
});
|
||||
id
|
||||
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,7 +272,8 @@ impl Painter {
|
|||
let gl = &self.gl;
|
||||
|
||||
for user_texture in &mut self.user_textures {
|
||||
if user_texture.texture.is_none() {
|
||||
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();
|
||||
|
@ -264,21 +304,10 @@ impl Painter {
|
|||
)
|
||||
.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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paint_jobs(
|
||||
|
@ -330,7 +359,8 @@ 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;
|
||||
|
@ -356,6 +386,12 @@ impl Painter {
|
|||
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(())
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
trigger_fetch |= ui.button("GET").clicked;
|
||||
|
||||
if trigger_fetch {
|
||||
if ui_url(ui, &mut self.url) {
|
||||
let (sender, receiver) = std::sync::mpsc::channel();
|
||||
self.receivers.push(receiver);
|
||||
self.in_progress = Some(receiver);
|
||||
let url = self.url.clone();
|
||||
|
||||
let future = async move {
|
||||
let result = egui_web::fetch::get_text(&url).await;
|
||||
sender.send(result).ok();
|
||||
egui_web::spawn_future(async move {
|
||||
sender.send(egui_web::fetch::get(&url).await).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();
|
||||
|
||||
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) {
|
||||
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: {} ({})",
|
||||
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
|
||||
));
|
||||
|
||||
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(&response.body);
|
||||
ui.monospace(text);
|
||||
});
|
||||
} else {
|
||||
ui.monospace("[binary]");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue