Browse Source

Replace all http code in epi/eframe/egui_glium/egui_web with ehttp (#697)

I've extracted all the http request code and turned it
into its own crate at <https://github.com/emilk/ehttp>.

There was never a reason for the HTTP request library to be part of
`eframe`. Much better to have it as its own crate!
pull/698/head
Emil Ernerfeldt 3 years ago
committed by GitHub
parent
commit
9598596bdc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      Cargo.lock
  2. 1
      eframe/CHANGELOG.md
  3. 1
      eframe/Cargo.toml
  4. 3
      egui/Cargo.toml
  5. 2
      egui_demo_app/Cargo.toml
  6. 3
      egui_demo_lib/Cargo.toml
  7. 2
      egui_demo_lib/src/apps/demo/code_editor.rs
  8. 28
      egui_demo_lib/src/apps/http_app.rs
  9. 1
      egui_glium/CHANGELOG.md
  10. 4
      egui_glium/Cargo.toml
  11. 9
      egui_glium/src/backend.rs
  12. 70
      egui_glium/src/http.rs
  13. 2
      egui_glium/src/lib.rs
  14. 1
      egui_web/CHANGELOG.md
  15. 8
      egui_web/Cargo.toml
  16. 8
      egui_web/src/backend.rs
  17. 97
      egui_web/src/http.rs
  18. 2
      egui_web/src/lib.rs
  19. 3
      emath/Cargo.toml
  20. 3
      epaint/Cargo.toml
  21. 1
      epi/Cargo.toml
  22. 115
      epi/src/lib.rs

15
Cargo.lock

@ -810,6 +810,7 @@ version = "0.14.0"
dependencies = [
"criterion",
"egui",
"ehttp",
"epi",
"image",
"serde",
@ -830,7 +831,6 @@ dependencies = [
"ron",
"serde",
"tts",
"ureq",
"webbrowser",
]
@ -849,6 +849,19 @@ dependencies = [
"web-sys",
]
[[package]]
name = "ehttp"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b078e2305de4c998700ac152b3e7a358d7fbe77e15b3b1cd2c44a8b82176124f"
dependencies = [
"js-sys",
"ureq",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "either"
version = "1.6.1"

1
eframe/CHANGELOG.md

@ -3,6 +3,7 @@ All notable changes to the `eframe` crate.
## Unreleased
* Remove "http" feature (use https://github.com/emilk/ehttp instead!).
## 0.14.0 - 2021-08-24

1
eframe/Cargo.toml

@ -40,7 +40,6 @@ default = ["default_fonts"]
# If set, egui will use `include_bytes!` to bundle some fonts.
# If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["egui/default_fonts"]
http = ["egui_glium/http", "egui_web/http"]
persistence = ["epi/persistence", "egui_glium/persistence", "egui_web/persistence"]
screen_reader = ["egui_glium/screen_reader", "egui_web/screen_reader"] # experimental
time = ["egui_glium/time"] # for seconds_since_midnight

3
egui/Cargo.toml

@ -17,6 +17,9 @@ include = [
"Cargo.toml",
]
[package.metadata.docs.rs]
all-features = true
[lib]
[dependencies]

2
egui_demo_app/Cargo.toml

@ -15,7 +15,7 @@ egui_demo_lib = { version = "0.14.0", path = "../egui_demo_lib", features = ["ex
[features]
default = ["persistence"]
http = ["eframe/http", "egui_demo_lib/http"]
http = ["egui_demo_lib/http"]
persistence = ["eframe/persistence", "egui_demo_lib/persistence"]
screen_reader = ["eframe/screen_reader"] # experimental
syntax_highlighting = ["egui_demo_lib/syntax_highlighting"]

3
egui_demo_lib/Cargo.toml

@ -28,6 +28,7 @@ epi = { version = "0.14.0", path = "../epi" }
unicode_names2 = { version = "0.4.0", default-features = false }
# feature "http":
ehttp = { version = "0.1.0", optional = true }
image = { version = "0.23", default-features = false, features = ["jpeg", "png"], optional = true }
# feature "syntax_highlighting":
@ -41,7 +42,7 @@ criterion = { version = "0.3", default-features = false }
[features]
default = []
http = ["image", "epi/http"]
http = ["ehttp", "image"]
persistence = ["egui/persistence", "epi/persistence", "serde"]
syntax_highlighting = ["syntect"]

2
egui_demo_lib/src/apps/demo/code_editor.rs

@ -65,7 +65,7 @@ impl super::View for CodeEditor {
} else {
ui.horizontal_wrapped(|ui|{
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("Compile the demo with the 'syntect' feature to enable much nicer syntax highlighting using ");
ui.label("Compile the demo with the 'syntax_highlighting' feature to enable much nicer syntax highlighting using ");
ui.hyperlink_to("syntect", "https://github.com/trishume/syntect");
ui.label(".");
});

28
egui_demo_lib/src/apps/http_app.rs

@ -1,9 +1,8 @@
use epi::http::{Request, Response};
use std::sync::mpsc::Receiver;
struct Resource {
/// HTTP response
response: Response,
response: ehttp::Response,
text: Option<String>,
@ -15,7 +14,7 @@ struct Resource {
}
impl Resource {
fn from_response(response: Response) -> Self {
fn from_response(response: ehttp::Response) -> Self {
let content_type = response.content_type().unwrap_or_default();
let image = if content_type.starts_with("image/") {
Image::decode(&response.bytes)
@ -54,7 +53,7 @@ pub struct HttpApp {
request_body: String,
#[cfg_attr(feature = "persistence", serde(skip))]
in_progress: Option<Receiver<Result<Response, String>>>,
in_progress: Option<Receiver<Result<ehttp::Response, String>>>,
#[cfg_attr(feature = "persistence", serde(skip))]
result: Option<Result<Resource, String>>,
@ -94,9 +93,15 @@ impl epi::App for HttpApp {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("HTTP Fetch Example");
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("HTTP requests made using ");
ui.hyperlink_to("ehttp", "https://www.github.com/emilk/ehttp");
ui.label(".");
});
ui.add(egui::github_link_file!(
"https://github.com/emilk/egui/blob/master/",
"(source code)"
"(demo source code)"
));
if let Some(request) = ui_url(
@ -110,7 +115,7 @@ impl epi::App for HttpApp {
let (sender, receiver) = std::sync::mpsc::channel();
self.in_progress = Some(receiver);
frame.http_fetch(request, move |response| {
ehttp::fetch(request, move |response| {
sender.send(response).ok();
repaint_signal.request_repaint();
});
@ -144,7 +149,7 @@ fn ui_url(
url: &mut String,
method: &mut Method,
request_body: &mut String,
) -> Option<Request> {
) -> Option<ehttp::Request> {
let mut trigger_fetch = false;
egui::Grid::new("request_params").show(ui, |ui| {
@ -202,8 +207,8 @@ fn ui_url(
if trigger_fetch {
Some(match *method {
Method::Get => Request::get(url),
Method::Post => Request::post(url, request_body),
Method::Get => ehttp::Request::get(url),
Method::Post => ehttp::Request::post(url, request_body),
})
} else {
None
@ -284,7 +289,7 @@ fn ui_resource(
// Syntax highlighting:
#[cfg(feature = "syntect")]
fn syntax_highlighting(response: &Response, text: &str) -> Option<ColoredText> {
fn syntax_highlighting(response: &ehttp::Response, text: &str) -> Option<ColoredText> {
let extension_and_rest: Vec<&str> = response.url.rsplitn(2, '.').collect();
let extension = extension_and_rest.get(0)?;
ColoredText::text_with_extension(text, extension)
@ -361,6 +366,7 @@ impl ColoredText {
}
}
#[cfg(feature = "syntect")]
fn as_byte_range(whole: &str, range: &str) -> std::ops::Range<usize> {
let whole_start = whole.as_ptr() as usize;
let range_start = range.as_ptr() as usize;
@ -371,7 +377,7 @@ fn as_byte_range(whole: &str, range: &str) -> std::ops::Range<usize> {
}
#[cfg(not(feature = "syntect"))]
fn syntax_highlighting(_: &Response, _: &str) -> Option<ColoredText> {
fn syntax_highlighting(_: &ehttp::Response, _: &str) -> Option<ColoredText> {
None
}
#[cfg(not(feature = "syntect"))]

1
egui_glium/CHANGELOG.md

@ -4,6 +4,7 @@ All notable changes to the `egui_glium` integration will be noted in this file.
## Unreleased
* Remove "http" feature (use https://github.com/emilk/ehttp instead!).
## 0.14.0 - 2021-08-24

4
egui_glium/Cargo.toml

@ -28,9 +28,6 @@ epi = { version = "0.14.0", path = "../epi" }
glium = "0.30"
webbrowser = "0.5"
# feature "http":
ureq = { version = "2.0", optional = true }
# feature "persistence":
directories-next = { version = "2", optional = true }
ron = { version = "0.6", optional = true }
@ -48,7 +45,6 @@ default = ["default_fonts"]
# If set, egui will use `include_bytes!` to bundle some fonts.
# If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["egui/default_fonts"]
http = ["epi/http", "ureq"]
persistence = [
"directories-next",
"egui/persistence",

9
egui_glium/src/backend.rs

@ -177,9 +177,6 @@ pub fn run(mut app: Box<dyn epi::App>, native_options: epi::NativeOptions) {
#[allow(unused_mut)]
let mut storage = create_storage(app.name());
#[cfg(feature = "http")]
let http = std::sync::Arc::new(crate::http::GliumHttp {});
let window_settings = deserialize_window_settings(&storage);
let mut event_loop = glutin::event_loop::EventLoop::with_user_event();
let icon = native_options.icon_data.clone().and_then(load_icon);
@ -198,8 +195,6 @@ pub fn run(mut app: Box<dyn epi::App>, native_options: epi::NativeOptions) {
let mut frame = epi::backend::FrameBuilder {
info: integration_info(&display, None),
tex_allocator: painter,
#[cfg(feature = "http")]
http: http.clone(),
output: &mut app_output,
repaint_signal: repaint_signal.clone(),
}
@ -222,8 +217,6 @@ pub fn run(mut app: Box<dyn epi::App>, native_options: epi::NativeOptions) {
let mut frame = epi::backend::FrameBuilder {
info: integration_info(&display, None),
tex_allocator: painter,
#[cfg(feature = "http")]
http: http.clone(),
output: &mut app_output,
repaint_signal: repaint_signal.clone(),
}
@ -319,8 +312,6 @@ pub fn run(mut app: Box<dyn epi::App>, native_options: epi::NativeOptions) {
let mut frame = epi::backend::FrameBuilder {
info: integration_info(&display, previous_frame_time),
tex_allocator: painter,
#[cfg(feature = "http")]
http: http.clone(),
output: &mut app_output,
repaint_signal: repaint_signal.clone(),
}

70
egui_glium/src/http.rs

@ -1,70 +0,0 @@
use std::collections::BTreeMap;
pub use epi::http::{Request, Response};
/// NOTE: Ok(..) is returned on network error.
/// Err is only for failure to use the fetch api.
pub fn fetch_blocking(request: &Request) -> Result<Response, String> {
let mut req = ureq::request(&request.method, &request.url);
for header in &request.headers {
req = req.set(header.0, header.1);
}
let resp = if request.body.is_empty() {
req.call()
} else {
req.send_bytes(&request.body)
};
let (ok, resp) = match resp {
Ok(resp) => (true, resp),
Err(ureq::Error::Status(_, resp)) => (false, resp), // Still read the body on e.g. 404
Err(ureq::Error::Transport(error)) => return Err(error.to_string()),
};
let url = resp.get_url().to_owned();
let status = resp.status();
let status_text = resp.status_text().to_owned();
let mut headers = BTreeMap::new();
for key in &resp.headers_names() {
if let Some(value) = resp.header(key) {
// lowercase for easy lookup
headers.insert(key.to_ascii_lowercase(), value.to_owned());
}
}
let mut reader = resp.into_reader();
let mut bytes = vec![];
use std::io::Read;
reader
.read_to_end(&mut bytes)
.map_err(|err| err.to_string())?;
let response = Response {
url,
ok,
status,
status_text,
bytes,
headers,
};
Ok(response)
}
// ----------------------------------------------------------------------------
pub(crate) struct GliumHttp {}
impl epi::backend::Http for GliumHttp {
fn fetch_dyn(
&self,
request: Request,
on_done: Box<dyn FnOnce(Result<Response, String>) + Send>,
) {
std::thread::spawn(move || {
let result = crate::http::fetch_blocking(&request);
on_done(result)
});
}
}

2
egui_glium/src/lib.rs

@ -19,8 +19,6 @@
#![allow(clippy::manual_range_contains, clippy::single_match)]
mod backend;
#[cfg(feature = "http")]
pub mod http;
mod painter;
#[cfg(feature = "persistence")]
pub mod persistence;

1
egui_web/CHANGELOG.md

@ -4,6 +4,7 @@ All notable changes to the `egui_web` integration will be noted in this file.
## Unreleased
* Remove "http" feature (use https://github.com/emilk/ehttp instead!).
## 0.14.1 - 2021-08-28

8
egui_web/Cargo.toml

@ -42,14 +42,6 @@ default = ["default_fonts"]
# If set, egui will use `include_bytes!` to bundle some fonts.
# If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["egui/default_fonts"]
http = [
"epi/http",
"web-sys/Headers",
"web-sys/Request",
"web-sys/RequestInit",
"web-sys/RequestMode",
"web-sys/Response",
]
persistence = ["egui/persistence", "ron", "serde"]
screen_reader = ["tts"] # experimental

8
egui_web/src/backend.rs

@ -137,8 +137,6 @@ pub struct AppRunner {
prefer_dark_mode: Option<bool>,
last_save_time: f64,
screen_reader: crate::screen_reader::ScreenReader,
#[cfg(feature = "http")]
http: Arc<http::WebHttp>,
pub(crate) last_text_cursor_pos: Option<egui::Pos2>,
}
@ -165,8 +163,6 @@ impl AppRunner {
prefer_dark_mode,
last_save_time: now_sec(),
screen_reader: Default::default(),
#[cfg(feature = "http")]
http: Arc::new(http::WebHttp {}),
last_text_cursor_pos: None,
};
@ -175,8 +171,6 @@ impl AppRunner {
let mut frame = epi::backend::FrameBuilder {
info: runner.integration_info(),
tex_allocator: runner.web_backend.painter.as_tex_allocator(),
#[cfg(feature = "http")]
http: runner.http.clone(),
output: &mut app_output,
repaint_signal: runner.needs_repaint.clone(),
}
@ -247,8 +241,6 @@ impl AppRunner {
let mut frame = epi::backend::FrameBuilder {
info: self.integration_info(),
tex_allocator: self.web_backend.painter.as_tex_allocator(),
#[cfg(feature = "http")]
http: self.http.clone(),
output: &mut app_output,
repaint_signal: self.needs_repaint.clone(),
}

97
egui_web/src/http.rs

@ -1,97 +0,0 @@
use wasm_bindgen::prelude::*;
pub use epi::http::{Request, Response};
/// NOTE: Ok(..) is returned on network error.
/// Err is only for failure to use the fetch api.
pub async fn fetch_async(request: &Request) -> Result<Response, String> {
fetch_jsvalue(request)
.await
.map_err(|err| err.as_string().unwrap_or(format!("{:#?}", err)))
}
/// NOTE: Ok(..) is returned on network error.
/// Err is only for failure to use the fetch api.
async fn fetch_jsvalue(request: &Request) -> Result<Response, JsValue> {
// https://rustwasm.github.io/wasm-bindgen/examples/fetch.html
// https://github.com/seanmonstar/reqwest/blob/master/src/wasm/client.rs
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
let mut opts = web_sys::RequestInit::new();
opts.method(&request.method);
opts.mode(web_sys::RequestMode::Cors);
if !request.body.is_empty() {
let body_bytes: &[u8] = &request.body;
let body_array: js_sys::Uint8Array = body_bytes.into();
let js_value: &JsValue = body_array.as_ref();
opts.body(Some(js_value));
}
let js_request = web_sys::Request::new_with_str_and_init(&request.url, &opts)?;
for h in &request.headers {
js_request.headers().set(h.0, h.1)?;
}
let window = web_sys::window().unwrap();
let response = JsFuture::from(window.fetch_with_request(&js_request)).await?;
let response: web_sys::Response = response.dyn_into()?;
let array_buffer = JsFuture::from(response.array_buffer()?).await?;
let uint8_array = js_sys::Uint8Array::new(&array_buffer);
let bytes = uint8_array.to_vec();
// https://developer.mozilla.org/en-US/docs/Web/API/Headers
// "Note: When Header values are iterated over, [...] values from duplicate header names are combined."
let mut headers = std::collections::BTreeMap::<String, String>::new();
let js_headers: web_sys::Headers = response.headers();
let js_iter = js_sys::try_iter(&js_headers)
.expect("headers try_iter")
.expect("headers have an iterator");
for item in js_iter {
let item = item.expect("headers iterator");
let array: js_sys::Array = item.into();
let v: Vec<JsValue> = array.to_vec();
let mut key = v[0]
.as_string()
.ok_or_else(|| JsValue::from_str("headers name"))?;
let value = v[1]
.as_string()
.ok_or_else(|| JsValue::from_str("headers value"))?;
// for easy lookup
key.make_ascii_lowercase();
headers.insert(key, value);
}
Ok(Response {
url: response.url(),
ok: response.ok(),
status: response.status(),
status_text: response.status_text(),
bytes,
headers,
})
}
// ----------------------------------------------------------------------------
pub(crate) struct WebHttp {}
impl epi::backend::Http for WebHttp {
fn fetch_dyn(
&self,
request: Request,
on_done: Box<dyn FnOnce(Result<Response, String>) + Send>,
) {
crate::spawn_future(async move {
let result = crate::http::fetch_async(&request).await;
on_done(result)
});
}
}

2
egui_web/src/lib.rs

@ -22,8 +22,6 @@
#![warn(clippy::all, rust_2018_idioms)]
pub mod backend;
#[cfg(feature = "http")]
pub mod http;
mod painter;
pub mod screen_reader;
pub mod webgl1;

3
emath/Cargo.toml

@ -17,6 +17,9 @@ include = [
"Cargo.toml",
]
[package.metadata.docs.rs]
all-features = true
[lib]
[dependencies]

3
epaint/Cargo.toml

@ -19,6 +19,9 @@ include = [
"fonts/*.txt",
]
[package.metadata.docs.rs]
all-features = true
[lib]
[dependencies]

1
epi/Cargo.toml

@ -29,5 +29,4 @@ serde = { version = "1", optional = true }
[features]
default = []
http = []
persistence = ["ron", "serde"]

115
epi/src/lib.rs

@ -1,6 +1,6 @@
//! Backend-agnostic interface for writing apps using [`egui`].
//!
//! `epi` provides interfaces for window management, serialization and http requests.
//! `epi` provides interfaces for window management and serialization.
//! An app written for `epi` can then be plugged into [`eframe`](https://docs.rs/eframe),
//! the egui framework crate.
//!
@ -221,7 +221,7 @@ pub struct IconData {
/// Represents the surroundings of your app.
///
/// It provides methods to inspect the surroundings (are we on the web?),
/// allocate textures, do http requests, and change settings (e.g. window size).
/// allocate textures, and change settings (e.g. window size).
pub struct Frame<'a>(backend::FrameBuilder<'a>);
impl<'a> Frame<'a> {
@ -255,19 +255,6 @@ impl<'a> Frame<'a> {
pub fn repaint_signal(&self) -> std::sync::Arc<dyn RepaintSignal> {
self.0.repaint_signal.clone()
}
/// Very simple Http fetch API.
/// Calls the given callback when done.
///
/// You must enable the "http" feature for this.
#[cfg(feature = "http")]
pub fn http_fetch(
&self,
request: http::Request,
on_done: impl 'static + Send + FnOnce(Result<http::Response, http::Error>),
) {
self.0.http.fetch_dyn(request, Box::new(on_done))
}
}
/// Information about the web environment (if applicable).
@ -374,114 +361,16 @@ pub const APP_KEY: &str = "app";
// ----------------------------------------------------------------------------
#[cfg(feature = "http")]
/// `epi` supports simple HTTP requests with [`Frame::http_fetch`].
///
/// You must enable the "http" feature for this.
pub mod http {
use std::collections::BTreeMap;
/// A simple http request.
pub struct Request {
/// "GET", …
pub method: String,
/// https://…
pub url: String,
/// The raw bytes.
pub body: Vec<u8>,
/// ("Accept", "*/*"), …
pub headers: BTreeMap<String, String>,
}
impl Request {
pub fn create_headers_map(headers: &[(&str, &str)]) -> BTreeMap<String, String> {
headers
.iter()
.map(|e| (e.0.to_owned(), e.1.to_owned()))
.collect()
}
/// Create a `GET` request with the given url.
#[allow(clippy::needless_pass_by_value)]
pub fn get(url: impl ToString) -> Self {
Self {
method: "GET".to_owned(),
url: url.to_string(),
body: vec![],
headers: Request::create_headers_map(&[("Accept", "*/*")]),
}
}
/// Create a `POST` request with the given url and body.
#[allow(clippy::needless_pass_by_value)]
pub fn post(url: impl ToString, body: impl ToString) -> Self {
Self {
method: "POST".to_owned(),
url: url.to_string(),
body: body.to_string().into_bytes(),
headers: Request::create_headers_map(&[
("Accept", "*/*"),
("Content-Type", "text/plain; charset=utf-8"),
]),
}
}
}
/// Response from a completed HTTP request.
pub struct Response {
/// The URL we ended up at. This can differ from the request url when we have followed redirects.
pub url: String,
/// Did we get a 2xx response code?
pub ok: bool,
/// Status code (e.g. `404` for "File not found").
pub status: u16,
/// Status text (e.g. "File not found" for status code `404`).
pub status_text: String,
/// The raw bytes.
pub bytes: Vec<u8>,
pub headers: BTreeMap<String, String>,
}
impl Response {
pub fn text(&self) -> Option<String> {
String::from_utf8(self.bytes.clone()).ok()
}
pub fn content_type(&self) -> Option<String> {
self.headers.get("content-type").cloned()
}
}
/// Possible errors does NOT include e.g. 404, which is NOT considered an error.
pub type Error = String;
}
// ----------------------------------------------------------------------------
/// You only need to look here if you are writing a backend for `epi`.
pub mod backend {
use super::*;
/// Implements `Http` requests.
#[cfg(feature = "http")]
pub trait Http {
/// Calls the given callback when done.
fn fetch_dyn(
&self,
request: http::Request,
on_done: Box<dyn FnOnce(Result<http::Response, http::Error>) + Send>,
);
}
/// The data required by [`Frame`] each frame.
pub struct FrameBuilder<'a> {
/// Information about the integration.
pub info: IntegrationInfo,
/// A way to allocate textures (on integrations that support it).
pub tex_allocator: &'a mut dyn TextureAllocator,
/// Do http requests.
#[cfg(feature = "http")]
pub http: std::sync::Arc<dyn backend::Http>,
/// Where the app can issue commands back to the integration.
pub output: &'a mut AppOutput,
/// If you need to request a repaint from another thread, clone this and send it to that other thread.

Loading…
Cancel
Save