From 375e317547d17b27e60c0e0ce6c49ebbc4ea49cb Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 31 Dec 2020 14:31:11 +0100 Subject: [PATCH] Move http fetch api from eframe to epi --- eframe/src/lib.rs | 31 ----- egui_demo_lib/src/app.rs | 42 ++++--- egui_glium/src/backend.rs | 16 ++- egui_glium/src/http.rs | 17 +++ egui_web/src/backend.rs | 29 +++-- egui_web/src/http.rs | 17 +++ epi/src/lib.rs | 201 +++++++++++++++++++++++++-------- example_web/src/example_app.rs | 16 +-- 8 files changed, 240 insertions(+), 129 deletions(-) diff --git a/eframe/src/lib.rs b/eframe/src/lib.rs index cc84fd85..63752f09 100644 --- a/eframe/src/lib.rs +++ b/eframe/src/lib.rs @@ -52,34 +52,3 @@ pub fn start_web(canvas_id: &str, app: Box) -> Result<(), wasm_bin pub fn run_native(app: Box) { egui_glium::run(app) } - -// ---------------------------------------------------------------------------- - -pub mod http { - pub use epi::http::*; - - /// Do a HTTP request and call the callback when done. - pub fn fetch( - request: Request, - on_done: impl 'static + Send + FnOnce(Result), - ) { - fetch_dyn(request, Box::new(on_done)) - } - - fn fetch_dyn(request: Request, on_done: Box) + Send>) { - #[cfg(target_arch = "wasm32")] - { - egui_web::spawn_future(async move { - let result = egui_web::http::fetch_async(&request).await; - on_done(result) - }); - } - #[cfg(not(target_arch = "wasm32"))] - { - std::thread::spawn(move || { - let result = egui_glium::http::fetch_blocking(&request); - on_done(result) - }); - } - } -} diff --git a/egui_demo_lib/src/app.rs b/egui_demo_lib/src/app.rs index abe79996..b3dd01f3 100644 --- a/egui_demo_lib/src/app.rs +++ b/egui_demo_lib/src/app.rs @@ -192,8 +192,8 @@ pub struct DemoApp { } impl DemoApp { - fn backend_ui(&mut self, ui: &mut Ui, integration_context: &mut epi::IntegrationContext<'_>) { - let is_web = integration_context.info.web_info.is_some(); + fn backend_ui(&mut self, ui: &mut Ui, frame: &mut epi::Frame<'_>) { + let is_web = frame.is_web(); if is_web { ui.label("Egui is an immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL."); @@ -217,13 +217,16 @@ impl DemoApp { if !is_web { // web browsers have their own way of zooming, which egui_web respects ui.separator(); - integration_context.output.pixels_per_point = - self.pixels_per_point_ui(ui, &integration_context.info); + if let Some(new_pixels_per_point) = self.pixels_per_point_ui(ui, frame.info()) { + frame.set_pixels_per_point(new_pixels_per_point); + } } if !is_web { ui.separator(); - integration_context.output.quit |= ui.button("Quit").clicked; + if ui.button("Quit").clicked { + frame.quit(); + } } } @@ -293,12 +296,12 @@ impl epi::App for DemoApp { epi::set_value(storage, epi::APP_KEY, self); } - fn ui(&mut self, ctx: &CtxRef, integration_context: &mut epi::IntegrationContext<'_>) { + fn ui(&mut self, ctx: &CtxRef, frame: &mut epi::Frame<'_>) { self.frame_history - .on_new_frame(ctx.input().time, integration_context.info.cpu_usage); + .on_new_frame(ctx.input().time, frame.info().cpu_usage); - let web_location_hash = integration_context - .info + let web_location_hash = frame + .info() .web_info .as_ref() .map(|info| info.web_location_hash.clone()) @@ -311,7 +314,7 @@ impl epi::App for DemoApp { }; let demo_environment = crate::DemoEnvironment { - seconds_since_midnight: integration_context.info.seconds_since_midnight, + seconds_since_midnight: frame.info().seconds_since_midnight, link, }; @@ -323,18 +326,13 @@ impl epi::App for DemoApp { .. } = self; - demo_windows.ui( - ctx, - &demo_environment, - &mut integration_context.tex_allocator, - |ui| { - ui.separator(); - ui.checkbox(backend_window_open, "💻 Backend"); + demo_windows.ui(ctx, &demo_environment, frame.tex_allocator(), |ui| { + ui.separator(); + ui.checkbox(backend_window_open, "💻 Backend"); - ui.label(format!("{:.2} ms / frame", 1e3 * mean_frame_time)) - .on_hover_text("CPU usage."); - }, - ); + ui.label(format!("{:.2} ms / frame", 1e3 * mean_frame_time)) + .on_hover_text("CPU usage."); + }); let mut backend_window_open = self.backend_window_open; egui::Window::new("💻 Backend") @@ -342,7 +340,7 @@ impl epi::App for DemoApp { .scroll(false) .open(&mut backend_window_open) .show(ctx, |ui| { - self.backend_ui(ui, integration_context); + self.backend_ui(ui, frame); }); self.backend_window_open = backend_window_open; diff --git a/egui_glium/src/backend.rs b/egui_glium/src/backend.rs index c224607d..0dac7c98 100644 --- a/egui_glium/src/backend.rs +++ b/egui_glium/src/backend.rs @@ -124,6 +124,8 @@ pub fn run(mut app: Box) -> ! { let mut last_auto_save = Instant::now(); + let http = std::sync::Arc::new(crate::http::GliumHttp {}); + event_loop.run(move |event, _, control_flow| { let mut redraw = || { let frame_start = Instant::now(); @@ -134,7 +136,8 @@ pub fn run(mut app: Box) -> ! { )); ctx.begin_frame(input_state.raw.take()); - let mut integration_context = epi::IntegrationContext { + let mut app_output = epi::backend::AppOutput::default(); + let mut frame = epi::backend::FrameBuilder { info: epi::IntegrationInfo { web_info: None, cpu_usage: previous_frame_time, @@ -142,11 +145,12 @@ pub fn run(mut app: Box) -> ! { native_pixels_per_point: Some(native_pixels_per_point(&display)), }, tex_allocator: Some(&mut painter), - output: Default::default(), + http: http.clone(), + output: &mut app_output, repaint_signal: repaint_signal.clone(), - }; - app.ui(&ctx, &mut integration_context); - let app_output = integration_context.output; + } + .build(); + app.ui(&ctx, &mut frame); let (egui_output, paint_commands) = ctx.end_frame(); let paint_jobs = ctx.tessellate(paint_commands); @@ -161,7 +165,7 @@ pub fn run(mut app: Box) -> ! { ); { - let epi::AppOutput { + let epi::backend::AppOutput { quit, window_size, pixels_per_point, diff --git a/egui_glium/src/http.rs b/egui_glium/src/http.rs index 9752d5f7..704b1365 100644 --- a/egui_glium/src/http.rs +++ b/egui_glium/src/http.rs @@ -42,3 +42,20 @@ pub fn fetch_blocking(request: &Request) -> Result { }; Ok(response) } + +// ---------------------------------------------------------------------------- + +pub(crate) struct GliumHttp {} + +impl epi::backend::Http for GliumHttp { + fn fetch_dyn( + &self, + request: Request, + on_done: Box) + Send>, + ) { + std::thread::spawn(move || { + let result = crate::http::fetch_blocking(&request); + on_done(result) + }); + } +} diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs index b7bbfc2e..c3e5e86e 100644 --- a/egui_web/src/backend.rs +++ b/egui_web/src/backend.rs @@ -1,6 +1,7 @@ use crate::*; pub use egui::{pos2, Srgba}; +use http::WebHttp; // ---------------------------------------------------------------------------- @@ -137,12 +138,13 @@ impl epi::RepaintSignal for NeedRepaint { // ---------------------------------------------------------------------------- pub struct AppRunner { - pub web_backend: WebBackend, - pub input: WebInput, - pub app: Box, - pub needs_repaint: std::sync::Arc, - pub storage: LocalStorage, - pub last_save_time: f64, + web_backend: WebBackend, + pub(crate) input: WebInput, + app: Box, + pub(crate) needs_repaint: std::sync::Arc, + storage: LocalStorage, + last_save_time: f64, + http: Arc, } impl AppRunner { @@ -158,6 +160,7 @@ impl AppRunner { needs_repaint: Default::default(), storage, last_save_time: now_sec(), + http: Arc::new(WebHttp {}), }) } @@ -182,7 +185,8 @@ impl AppRunner { let raw_input = self.input.new_frame(canvas_size); self.web_backend.begin_frame(raw_input); - let mut integration_context = epi::IntegrationContext { + let mut app_output = epi::backend::AppOutput::default(); + let mut frame = epi::backend::FrameBuilder { info: epi::IntegrationInfo { web_info: Some(epi::WebInfo { web_location_hash: location_hash().unwrap_or_default(), @@ -192,18 +196,19 @@ impl AppRunner { native_pixels_per_point: Some(native_pixels_per_point()), }, tex_allocator: Some(&mut self.web_backend.painter), - output: Default::default(), + http: self.http.clone(), + output: &mut app_output, repaint_signal: self.needs_repaint.clone(), - }; + } + .build(); let egui_ctx = &self.web_backend.ctx; - self.app.ui(egui_ctx, &mut integration_context); - let app_output = integration_context.output; + self.app.ui(egui_ctx, &mut frame); let (egui_output, paint_jobs) = self.web_backend.end_frame()?; handle_output(&egui_output); { - let epi::AppOutput { + let epi::backend::AppOutput { quit: _, // Can't quit a web page window_size: _, // Can't resize a web page pixels_per_point: _, // Can't zoom from within the app (we respect the web browser's zoom level) diff --git a/egui_web/src/http.rs b/egui_web/src/http.rs index b81c7b71..9fb49c80 100644 --- a/egui_web/src/http.rs +++ b/egui_web/src/http.rs @@ -66,3 +66,20 @@ async fn fetch_jsvalue(request: &Request) -> Result { text, }) } + +// ---------------------------------------------------------------------------- + +pub(crate) struct WebHttp {} + +impl epi::backend::Http for WebHttp { + fn fetch_dyn( + &self, + request: Request, + on_done: Box) + Send>, + ) { + crate::spawn_future(async move { + let result = crate::http::fetch_async(&request).await; + on_done(result) + }); + } +} diff --git a/epi/src/lib.rs b/epi/src/lib.rs index 63bb832b..5ffb349f 100644 --- a/epi/src/lib.rs +++ b/epi/src/lib.rs @@ -1,6 +1,8 @@ -//! Backend-agnostic interface for writing apps using Egui. +//! Backend-agnostic interface for writing apps using [`egui`]. //! -//! Egui is a GUI library, which can be plugged in to e.g. a game engine. +//! [`egui`] is a GUI library, which can be plugged in to e.g. a game engine. +//! +//! Start by looking at the [`App`] trait, and implement [`App::ui`]. //! //! This crate provides a common interface for programming an app, using Egui, //! so you can then easily plug it in to a backend such as `egui_web` or `egui_glium`. @@ -44,10 +46,9 @@ future_incompatible, missing_crate_level_docs, missing_doc_code_examples, - // missing_docs, - nonstandard_style, + missing_docs, rust_2018_idioms, - unused_doc_comments, + unused_doc_comments )] pub use egui; // Re-export for user convenience @@ -57,15 +58,14 @@ pub use egui; // Re-export for user convenience /// Implement this trait to write apps that can be compiled both natively using the [`egui_glium`](https://crates.io/crates/egui_glium) crate, /// and deployed as a web site using the [`egui_web`](https://crates.io/crates/egui_web) crate. pub trait App { - /// The name of your App. - fn name(&self) -> &str; + /// Called each time the UI needs repainting, which may be many times per second. + /// Put your widgets into a [`egui::SidePanel`], [`egui::TopPanel`], [`egui::CentralPanel`], [`egui::Window`] or [`egui::Area`]. + fn ui(&mut self, ctx: &egui::CtxRef, frame: &mut Frame<'_>); - /// Background color for the app, e.g. what is sent to `gl.clearColor`. - /// This is the background of your windows if you don't set a central panel. - fn clear_color(&self) -> egui::Rgba { - // NOTE: a bright gray makes the shadows of the windows look weird. - egui::Srgba::from_rgb(12, 12, 12).into() - } + /// Called once before the first frame. + /// Allows you to do setup code and to call `ctx.set_fonts()`. + /// Optional. + fn setup(&mut self, _ctx: &egui::CtxRef) {} /// Called once on start. Allows you to restore state. fn load(&mut self, _storage: &dyn Storage) {} @@ -73,40 +73,88 @@ pub trait App { /// Called on shutdown, and perhaps at regular intervals. Allows you to save state. fn save(&mut self, _storage: &mut dyn Storage) {} + /// Called once on shutdown (before or after `save()`) + fn on_exit(&mut self) {} + + // --------- + // Settings: + + /// The name of your App. + fn name(&self) -> &str; + /// Time between automatic calls to `save()` fn auto_save_interval(&self) -> std::time::Duration { std::time::Duration::from_secs(30) } - /// Called once on shutdown (before or after `save()`) - fn on_exit(&mut self) {} - - /// Called once before the first frame. - /// Allows you to do setup code and to call `ctx.set_fonts()`. - /// Optional. - fn setup(&mut self, _ctx: &egui::CtxRef) {} - /// Returns true if this app window should be resizable. fn is_resizable(&self) -> bool { true } - /// Called each time the UI needs repainting, which may be many times per second. - /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`. - fn ui(&mut self, ctx: &egui::CtxRef, integration_context: &mut IntegrationContext<'_>); + /// Background color for the app, e.g. what is sent to `gl.clearColor`. + /// This is the background of your windows if you don't set a central panel. + fn clear_color(&self) -> egui::Rgba { + // NOTE: a bright gray makes the shadows of the windows look weird. + egui::Srgba::from_rgb(12, 12, 12).into() + } } -pub struct IntegrationContext<'a> { +/// 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). +pub struct Frame<'a>(backend::FrameBuilder<'a>); + +impl<'a> Frame<'a> { + /// True if you are in a web environment. + pub fn is_web(&self) -> bool { + self.info().web_info.is_some() + } + /// Information about the integration. - pub info: IntegrationInfo, + pub fn info(&self) -> &IntegrationInfo { + &self.0.info + } + /// A way to allocate textures (on integrations that support it). - pub tex_allocator: Option<&'a mut dyn TextureAllocator>, - /// Where the app can issue commands back to the integration. - pub output: AppOutput, + pub fn tex_allocator(&mut self) -> &mut Option<&'a mut dyn TextureAllocator> { + &mut self.0.tex_allocator + } + + /// Signal the app to stop/exit/quit the app (only works for native apps, not web apps). + /// The framework will NOT quick immediately, but at the end of the this frame. + pub fn quit(&mut self) { + self.0.output.quit = true; + } + + /// Set the desired inner size of the window (in egui points). + pub fn set_window_size(&mut self, size: egui::Vec2) { + self.0.output.window_size = Some(size); + } + + /// Change the `pixels_per_point` of [`egui`] to this next frame. + pub fn set_pixels_per_point(&mut self, pixels_per_point: f32) { + self.0.output.pixels_per_point = Some(pixels_per_point); + } + /// If you need to request a repaint from another thread, clone this and send it to that other thread. - pub repaint_signal: std::sync::Arc, + pub fn repaint_signal(&self) -> std::sync::Arc { + self.0.repaint_signal.clone() + } + + /// Very simple Http fetch API. + /// Calls the given callback when done. + pub fn http_fetch( + &self, + request: http::Request, + on_done: impl 'static + Send + FnOnce(Result), + ) { + self.0.http.fetch_dyn(request, Box::new(on_done)) + } } +/// Information about the web environment #[derive(Clone, Debug)] pub struct WebInfo { /// e.g. "#fragment" part of "www.example.com/index.html#fragment" @@ -131,22 +179,9 @@ pub struct IntegrationInfo { pub native_pixels_per_point: Option, } -/// Action that can be taken by the user app. -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub struct AppOutput { - /// Set to `true` to stop the app. - /// This does nothing for web apps. - pub quit: bool, - - /// Set to some size to resize the outer window (e.g. glium window) to this size. - pub window_size: Option, - - /// If the app sets this, change the `pixels_per_point` of Egui to this next frame. - pub pixels_per_point: Option, -} - +/// How to allocate textures (images) to use in [`egui`]. pub trait TextureAllocator { - /// A.locate a new user texture. + /// Allocate a new user texture. fn alloc(&mut self) -> egui::TextureId; /// Set or change the pixels of a user texture. @@ -161,8 +196,9 @@ pub trait TextureAllocator { fn free(&mut self, id: egui::TextureId); } +/// How to signal the [`egui`] integration that a repaint is required. pub trait RepaintSignal: Send + Sync { - /// This signals the Egui integration that a repaint is required. + /// This signals the [`egui`] integration that a repaint is required. /// This is meant to be called when a background process finishes in an async context and/or background thread. fn request_repaint(&self); } @@ -174,7 +210,9 @@ pub trait RepaintSignal: Send + Sync { /// On the web this is backed by [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). /// On desktop this is backed by the file system. pub trait Storage { + /// Get the value for the given key. fn get_string(&self, key: &str) -> Option; + /// Set the value for the given key. fn set_string(&mut self, key: &str, value: String); /// write-to-disk or similar @@ -193,6 +231,7 @@ impl Storage for DummyStorage { fn flush(&mut self) {} } +/// Get an deserialize the JSON stored at the given key. #[cfg(feature = "serde_json")] pub fn get_value(storage: &dyn Storage, key: &str) -> Option { storage @@ -200,17 +239,20 @@ pub fn get_value(storage: &dyn Storage, key: &st .and_then(|value| serde_json::from_str(&value).ok()) } +/// Serialize the given value as JSON and store with the given key. #[cfg(feature = "serde_json")] pub fn set_value(storage: &mut dyn Storage, key: &str, value: &T) { storage.set_string(key, serde_json::to_string(value).unwrap()); } -/// storage key used for app +/// [`Storage`] key used for app pub const APP_KEY: &str = "app"; // ---------------------------------------------------------------------------- +/// `epi` supports simple HTTP requests with [`Frame::http_fetch`]. pub mod http { + /// A simple http requests. pub struct Request { /// "GET", … pub method: String, @@ -219,20 +261,24 @@ pub mod http { } impl Request { - pub fn get(url: String) -> Self { + /// Create a `GET` requests with the given url. + pub fn get(url: impl Into) -> Self { Self { method: "GET".to_owned(), - url, + url: url.into(), } } } - /// Response from an HTTP request for a very simple HTTP fetch API in `eframe`. + /// 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 tex (e.g. "File not found" for status code `404`). pub status_text: String, /// Content-Type header, or empty string if missing. @@ -245,4 +291,59 @@ pub mod http { /// ONLY if `header_content_type` starts with "text" and bytes is UTF-8. pub text: Option, } + + /// 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. + pub trait Http { + /// Calls the given callback when done. + fn fetch_dyn( + &self, + request: http::Request, + on_done: Box) + 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: Option<&'a mut dyn TextureAllocator>, + /// Do http requests. + pub http: std::sync::Arc, + /// 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. + pub repaint_signal: std::sync::Arc, + } + + impl<'a> FrameBuilder<'a> { + /// Wrap us in a [`Frame`] to send to [`App::ui`]. + pub fn build(self) -> Frame<'a> { + Frame(self) + } + } + + /// Action that can be taken by the user app. + #[derive(Clone, Copy, Debug, Default, PartialEq)] + pub struct AppOutput { + /// Set to `true` to stop the app. + /// This does nothing for web apps. + pub quit: bool, + + /// Set to some size to resize the outer window (e.g. glium window) to this size. + pub window_size: Option, + + /// If the app sets this, change the `pixels_per_point` of [`egui`] to this next frame. + pub pixels_per_point: Option, + } } diff --git a/example_web/src/example_app.rs b/example_web/src/example_app.rs index 9d8a94fc..2dd0a42a 100644 --- a/example_web/src/example_app.rs +++ b/example_web/src/example_app.rs @@ -56,7 +56,7 @@ impl epi::App for ExampleApp { /// Called each time the UI needs repainting, which may be many times per second. /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`. - fn ui(&mut self, ctx: &egui::CtxRef, integration_context: &mut epi::IntegrationContext) { + fn ui(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>) { if let Some(receiver) = &mut self.in_progress { // Are we there yet? if let Ok(result) = receiver.try_recv() { @@ -73,11 +73,11 @@ impl epi::App for ExampleApp { )); if let Some(url) = ui_url(ui, &mut self.url) { - let repaint_signal = integration_context.repaint_signal.clone(); + let repaint_signal = frame.repaint_signal(); let (sender, receiver) = std::sync::mpsc::channel(); self.in_progress = Some(receiver); - eframe::http::fetch(eframe::http::Request::get(url), move |response| { + frame.http_fetch(epi::http::Request::get(url), move |response| { sender.send(response).ok(); repaint_signal.request_repaint(); }); @@ -90,7 +90,7 @@ impl epi::App for ExampleApp { } else if let Some(result) = &self.result { match result { Ok(resource) => { - ui_resouce(ui, integration_context, &mut self.tex_mngr, resource); + ui_resouce(ui, frame, &mut self.tex_mngr, resource); } Err(error) => { // This should only happen if the fetch API isn't available or something similar. @@ -139,7 +139,7 @@ fn ui_url(ui: &mut egui::Ui, url: &mut String) -> Option { fn ui_resouce( ui: &mut egui::Ui, - integration_context: &mut epi::IntegrationContext, + frame: &mut epi::Frame<'_>, tex_mngr: &mut TexMngr, resource: &Resource, ) { @@ -171,7 +171,7 @@ fn ui_resouce( egui::ScrollArea::auto_sized().show(ui, |ui| { if let Some(image) = image { - if let Some(texture_id) = tex_mngr.texture(integration_context, &response.url, &image) { + if let Some(texture_id) = tex_mngr.texture(frame, &response.url, &image) { let size = egui::Vec2::new(image.size.0 as f32, image.size.1 as f32); ui.image(texture_id, size); } @@ -252,11 +252,11 @@ struct TexMngr { impl TexMngr { fn texture( &mut self, - integration_context: &mut epi::IntegrationContext, + frame: &mut epi::Frame<'_>, url: &str, image: &Image, ) -> Option { - let tex_allocator = integration_context.tex_allocator.as_mut()?; + let tex_allocator = frame.tex_allocator().as_mut()?; let texture_id = self.texture_id.unwrap_or_else(|| tex_allocator.alloc()); self.texture_id = Some(texture_id); if self.loaded_url != url {