diff --git a/Cargo.lock b/Cargo.lock index 334adc4b..6d12a171 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -427,12 +427,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "egui" version = "0.1.2" -source = "git+https://github.com/emilk/emigui#3552f7f82861fb10ae646c61f3c283256e79940e" +source = "git+https://github.com/emilk/emigui#e3d1d6c99ce37090d496b4f210aaef700235dd9b" dependencies = [ "ahash 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "rusttype 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.56 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -444,12 +445,13 @@ dependencies = [ "parking_lot 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "rusttype 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.56 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "egui_glium" version = "0.1.0" -source = "git+https://github.com/emilk/emigui#3552f7f82861fb10ae646c61f3c283256e79940e" +source = "git+https://github.com/emilk/emigui#e3d1d6c99ce37090d496b4f210aaef700235dd9b" dependencies = [ "chrono 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)", "clipboard 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -476,7 +478,7 @@ dependencies = [ [[package]] name = "egui_web" version = "0.1.0" -source = "git+https://github.com/emilk/emigui#3552f7f82861fb10ae646c61f3c283256e79940e" +source = "git+https://github.com/emilk/emigui#e3d1d6c99ce37090d496b4f210aaef700235dd9b" dependencies = [ "egui 0.1.2 (git+https://github.com/emilk/emigui)", "js-sys 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/demo_glium/src/main.rs b/demo_glium/src/main.rs index 843a41dd..b6d2474f 100644 --- a/demo_glium/src/main.rs +++ b/demo_glium/src/main.rs @@ -1,70 +1,11 @@ #![deny(warnings)] #![warn(clippy::all)] -use egui_glium::{persistence::Persistence, RunMode, Runner}; - -const APP_KEY: &str = "app"; - -#[derive(Default, serde::Deserialize, serde::Serialize)] -struct MyApp { - egui_demo_app: egui::DemoApp, - frames_painted: u64, -} - -impl egui_glium::App for MyApp { - fn ui(&mut self, ui: &mut egui::Ui, runner: &mut Runner) { - self.egui_demo_app.ui(ui, ""); - - use egui::*; - let mut ui = ui.centered_column(ui.available().width().min(480.0)); - ui.set_layout(Layout::vertical(Align::Min)); - ui.add(label!("Egui inside of Glium").text_style(TextStyle::Heading)); - if ui.add(Button::new("Quit")).clicked { - runner.quit(); - return; - } - - ui.add( - label!( - "CPU usage: {:.2} ms / frame (excludes painting)", - 1e3 * runner.cpu_time() - ) - .text_style(TextStyle::Monospace), - ); - - ui.separator(); - - ui.horizontal(|ui| { - let mut run_mode = runner.run_mode(); - ui.label("Run mode:"); - ui.radio_value("Continuous", &mut run_mode, RunMode::Continuous) - .tooltip_text("Repaint everything each frame"); - ui.radio_value("Reactive", &mut run_mode, RunMode::Reactive) - .tooltip_text("Repaint when there are animations or input (e.g. mouse movement)"); - runner.set_run_mode(run_mode); - }); - - if runner.run_mode() == RunMode::Continuous { - ui.add( - label!("Repainting the UI each frame. FPS: {:.1}", runner.fps()) - .text_style(TextStyle::Monospace), - ); - } else { - ui.label("Only running UI code when there are animations or input"); - } - - self.frames_painted += 1; - ui.label(format!("Total frames painted: {}", self.frames_painted)); - } - - fn on_exit(&mut self, persistence: &mut Persistence) { - persistence.set_value(APP_KEY, self); - } -} +use egui_glium::{storage::FileStorage, RunMode}; fn main() { let title = "Egui glium demo"; - let persistence = Persistence::from_path(".egui_demo_glium.json".into()); - let app: MyApp = persistence.get_value(APP_KEY).unwrap_or_default(); - egui_glium::run(title, RunMode::Reactive, persistence, app); + let storage = FileStorage::from_path(".egui_demo_glium.json".into()); + let app: egui::DemoApp = egui::app::get_value(&storage, egui::app::APP_KEY).unwrap_or_default(); + egui_glium::run(title, RunMode::Reactive, storage, app); } diff --git a/demo_web/src/lib.rs b/demo_web/src/lib.rs index bf070136..61498fb9 100644 --- a/demo_web/src/lib.rs +++ b/demo_web/src/lib.rs @@ -1,86 +1,14 @@ #![deny(warnings)] #![warn(clippy::all)] -use egui::{label, TextStyle}; - use wasm_bindgen::prelude::*; -// ---------------------------------------------------------------------------- - /// This is the entry-point for all the web-assembly. #[wasm_bindgen] pub fn start(canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> { - let backend = egui_web::Backend::new(canvas_id, egui_web::RunMode::Reactive)?; - let app = Box::new(MyApp::default()); + let backend = egui_web::WebBackend::new(canvas_id, egui_web::RunMode::Reactive)?; + let app = Box::new(egui::DemoApp::default()); let runner = egui_web::AppRunner::new(backend, app)?; egui_web::run(runner)?; Ok(()) } - -// ---------------------------------------------------------------------------- - -#[derive(Default)] -pub struct MyApp { - demo_app: egui::demos::DemoApp, - frames_painted: u64, -} - -impl MyApp { - fn window_ui(&mut self, ui: &mut egui::Ui, backend: &mut egui_web::Backend) { - ui.label("Egui is an immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL."); - ui.label( - "Everything you see is rendered as textured triangles. There is no DOM. There are no HTML elements." - ); - ui.label("This is not JavaScript. This is Rust, running at 60 FPS. This is the web page, reinvented with game tech."); - ui.label("This is also work in progress, and not ready for production... yet :)"); - ui.horizontal(|ui| { - ui.label("Project home page:"); - ui.hyperlink("https://github.com/emilk/emigui/"); - }); - ui.separator(); - - ui.add( - label!( - "CPU usage: {:.2} ms / frame (excludes painting)", - 1e3 * backend.cpu_time() - ) - .text_style(TextStyle::Monospace), - ); - - ui.separator(); - - ui.horizontal(|ui| { - let mut run_mode = backend.run_mode(); - ui.label("Run mode:"); - ui.radio_value("Continuous", &mut run_mode, egui_web::RunMode::Continuous) - .tooltip_text("Repaint everything each frame"); - ui.radio_value("Reactive", &mut run_mode, egui_web::RunMode::Reactive) - .tooltip_text("Repaint when there are animations or input (e.g. mouse movement)"); - backend.set_run_mode(run_mode); - }); - - if backend.run_mode() == egui_web::RunMode::Continuous { - ui.add( - label!("Repainting the UI each frame. FPS: {:.1}", backend.fps()) - .text_style(TextStyle::Monospace), - ); - } else { - ui.label("Only running UI code when there are animations or input"); - } - - self.frames_painted += 1; - ui.label(format!("Total frames painted: {}", self.frames_painted)); - } -} - -impl egui_web::App for MyApp { - fn ui(&mut self, ui: &mut egui::Ui, backend: &mut egui_web::Backend, info: &egui_web::WebInfo) { - egui::Window::new("Egui") - .default_width(500.0) - .show(ui.ctx(), |ui| { - self.window_ui(ui, backend); - }); - - self.demo_app.ui(ui, &info.web_location_hash); - } -} diff --git a/docs/demo_web.js b/docs/demo_web.js index 406a442d..5d650fe9 100644 --- a/docs/demo_web.js +++ b/docs/demo_web.js @@ -208,27 +208,27 @@ function makeMutClosure(arg0, arg1, dtor, f) { return real; } function __wbg_adapter_26(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h22fd33d9f501a695(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h426caa8a7645dc38(arg0, arg1, addHeapObject(arg2)); } function __wbg_adapter_29(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h22fd33d9f501a695(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h426caa8a7645dc38(arg0, arg1, addHeapObject(arg2)); } function __wbg_adapter_32(arg0, arg1) { - wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h5402719cc6dde927(arg0, arg1); + wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb0f876788be788af(arg0, arg1); } function __wbg_adapter_35(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h22fd33d9f501a695(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h426caa8a7645dc38(arg0, arg1, addHeapObject(arg2)); } -function __wbg_adapter_38(arg0, arg1) { - wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h84da5f062b972f09(arg0, arg1); +function __wbg_adapter_38(arg0, arg1, arg2) { + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h426caa8a7645dc38(arg0, arg1, addHeapObject(arg2)); } -function __wbg_adapter_41(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h22fd33d9f501a695(arg0, arg1, addHeapObject(arg2)); +function __wbg_adapter_41(arg0, arg1) { + wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h400d42c2242ce5bd(arg0, arg1); } /** @@ -304,6 +304,10 @@ async function init(input) { imports.wbg.__wbindgen_object_drop_ref = function(arg0) { takeObject(arg0); }; + imports.wbg.__wbindgen_string_new = function(arg0, arg1) { + var ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); + }; imports.wbg.__wbindgen_cb_forget = function(arg0) { takeObject(arg0); }; @@ -316,10 +320,6 @@ async function init(input) { var ret = false; return ret; }; - imports.wbg.__wbindgen_string_new = function(arg0, arg1) { - var ret = getStringFromWasm0(arg0, arg1); - return addHeapObject(ret); - }; imports.wbg.__wbg_instanceof_Window_0e8decd0a6179699 = function(arg0) { var ret = getObject(arg0) instanceof Window; return ret; @@ -717,28 +717,28 @@ async function init(input) { var ret = wasm.memory; return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper380 = function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 68, __wbg_adapter_26); + imports.wbg.__wbindgen_closure_wrapper364 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 76, __wbg_adapter_35); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper366 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 76, __wbg_adapter_38); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper363 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 76, __wbg_adapter_41); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper368 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 76, __wbg_adapter_26); return addHeapObject(ret); }; imports.wbg.__wbindgen_closure_wrapper370 = function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 68, __wbg_adapter_32); + var ret = makeMutClosure(arg0, arg1, 76, __wbg_adapter_29); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper378 = function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 68, __wbg_adapter_41); - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_closure_wrapper376 = function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 68, __wbg_adapter_38); - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_closure_wrapper374 = function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 68, __wbg_adapter_35); - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_closure_wrapper371 = function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 68, __wbg_adapter_29); + imports.wbg.__wbindgen_closure_wrapper373 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 76, __wbg_adapter_32); return addHeapObject(ret); }; diff --git a/docs/demo_web_bg.wasm b/docs/demo_web_bg.wasm index 7faf71a0..b4c002e1 100644 Binary files a/docs/demo_web_bg.wasm and b/docs/demo_web_bg.wasm differ diff --git a/egui/Cargo.toml b/egui/Cargo.toml index 0892ff80..ea623745 100644 --- a/egui/Cargo.toml +++ b/egui/Cargo.toml @@ -19,9 +19,10 @@ ahash = "0.4" parking_lot = "0.11" rusttype = "0.9" serde = { version = "1", features = ["derive"], optional = true } +serde_json = { version = "1", optional = true } [features] -with_serde = ["serde"] +with_serde = ["serde", "serde_json"] [dev-dependencies] criterion = { version = "0.3", default-features = false } diff --git a/egui/src/app.rs b/egui/src/app.rs new file mode 100644 index 00000000..c1889788 --- /dev/null +++ b/egui/src/app.rs @@ -0,0 +1,79 @@ +//! Traits and helper for writing Egui apps. +//! +//! Egui can be used as a library, but you can also use it as a framework to write apps in. +//! This module defined the `App` trait that can be implemented and used with the `egui_web` and `egui_glium` crates. + +use crate::Ui; + +/// Implement this trait to write apps that can be compiled both natively using the `egui_glium` crate, +/// and deployed as a web site using the `egui_web` crate. +pub trait App { + /// Called each time the UI needs repainting, which may be many times per second. + fn ui(&mut self, ui: &mut Ui, backend: &mut dyn Backend); + + /// Called once on shutdown. Allows you to save state. + fn on_exit(&mut self, _storage: &mut dyn Storage) {} +} + +// TODO: replace with manually calling `egui::Context::request_repaint()` each frame. +/// How the backend runs the app +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RunMode { + /// Rapint the UI all the time (at the display refresh rate of e.g. 60 Hz). + /// This is good for games where things are constantly moving. + /// This can also be achieved with `RunMode::Reactive` combined with calling `egui::Context::request_repaint()` each frame. + Continuous, + + /// Only repaint when there are animations or input (mouse movement, keyboard input etc). + /// This saves CPU. + Reactive, +} + +pub struct WebInfo { + /// e.g. "#fragment" part of "www.example.com/index.html#fragment" + pub web_location_hash: String, +} + +pub trait Backend { + fn run_mode(&self) -> RunMode; + fn set_run_mode(&mut self, run_mode: RunMode); + + /// If the app is running in a Web context, this returns information about the environment. + fn web_info(&self) -> Option { + None + } + + /// excludes painting + fn cpu_time(&self) -> f32; + + /// Smoothed frames per second + fn fps(&self) -> f32; + + /// Signal the backend that we'd like to exit the app now. + /// This does nothing for web apps.s + fn quit(&mut self) {} +} + +/// A place where you can store custom data in a way that persists when you restart the app. +/// +/// 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 { + fn get_string(&self, key: &str) -> Option<&str>; + fn set_string(&mut self, key: &str, value: String); +} + +#[cfg(feature = "with_serde")] +pub fn get_value(storage: &dyn Storage, key: &str) -> Option { + storage + .get_string(key) + .and_then(|value| serde_json::from_str(value).ok()) +} + +#[cfg(feature = "with_serde")] +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 +pub const APP_KEY: &str = "app"; diff --git a/egui/src/demos/app.rs b/egui/src/demos/app.rs index e20c4ae7..c1862adc 100644 --- a/egui/src/demos/app.rs +++ b/egui/src/demos/app.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::{color::*, containers::*, demos::FractalClock, paint::*, widgets::*, *}; +use crate::{app, color::*, containers::*, demos::FractalClock, paint::*, widgets::*, *}; // ---------------------------------------------------------------------------- @@ -12,6 +12,7 @@ pub struct DemoApp { open_windows: OpenWindows, demo_window: DemoWindow, fractal_clock: FractalClock, + num_frames_painted: u64, } impl DemoApp { @@ -68,6 +69,84 @@ impl DemoApp { fractal_clock.window(ctx, &mut open_windows.fractal_clock); } + + fn backend_ui(&mut self, ui: &mut Ui, backend: &mut dyn app::Backend) { + let is_web = backend.web_info().is_some(); + + if is_web { + ui.label("Egui is an immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL."); + ui.label( + "Everything you see is rendered as textured triangles. There is no DOM. There are no HTML elements." + ); + ui.label("This is not JavaScript. This is Rust, running at 60 FPS. This is the web page, reinvented with game tech."); + ui.label("This is also work in progress, and not ready for production... yet :)"); + ui.horizontal(|ui| { + ui.label("Project home page:"); + ui.hyperlink("https://github.com/emilk/emigui/"); + }); + } else { + ui.add(label!("Egui").text_style(TextStyle::Heading)); + if ui.add(Button::new("Quit")).clicked { + backend.quit(); + return; + } + } + + ui.separator(); + + ui.add( + label!( + "CPU usage: {:.2} ms / frame (excludes painting)", + 1e3 * backend.cpu_time() + ) + .text_style(TextStyle::Monospace), + ); + + ui.separator(); + + ui.horizontal(|ui| { + let mut run_mode = backend.run_mode(); + ui.label("Run mode:"); + ui.radio_value("Continuous", &mut run_mode, app::RunMode::Continuous) + .tooltip_text("Repaint everything each frame"); + ui.radio_value("Reactive", &mut run_mode, app::RunMode::Reactive) + .tooltip_text("Repaint when there are animations or input (e.g. mouse movement)"); + backend.set_run_mode(run_mode); + }); + + if backend.run_mode() == app::RunMode::Continuous { + ui.add( + label!("Repainting the UI each frame. FPS: {:.1}", backend.fps()) + .text_style(TextStyle::Monospace), + ); + } else { + ui.label("Only running UI code when there are animations or input"); + } + + self.num_frames_painted += 1; + ui.label(format!("Total frames painted: {}", self.num_frames_painted)); + } +} + +impl app::App for DemoApp { + fn ui(&mut self, ui: &mut Ui, backend: &mut dyn app::Backend) { + Window::new("Backend") + .default_width(500.0) + .show(ui.ctx(), |ui| { + self.backend_ui(ui, backend); + }); + + let web_info = backend.web_info(); + let web_location_hash = web_info + .as_ref() + .map(|info| info.web_location_hash.as_str()) + .unwrap_or_default(); + self.ui(ui, web_location_hash); + } + + fn on_exit(&mut self, storage: &mut dyn app::Storage) { + app::set_value(storage, app::APP_KEY, self); + } } // ---------------------------------------------------------------------------- diff --git a/egui/src/lib.rs b/egui/src/lib.rs index baa784b0..e3c1af9d 100644 --- a/egui/src/lib.rs +++ b/egui/src/lib.rs @@ -22,6 +22,7 @@ rust_2018_idioms, )] +pub mod app; pub mod containers; mod context; pub mod demos; diff --git a/egui_glium/src/runner.rs b/egui_glium/src/backend.rs similarity index 70% rename from egui_glium/src/runner.rs rename to egui_glium/src/backend.rs index abbf8f71..a4a07bf0 100644 --- a/egui_glium/src/runner.rs +++ b/egui_glium/src/backend.rs @@ -1,38 +1,22 @@ use std::time::Instant; use crate::{ - persistence::{Persistence, WindowSettings}, + storage::{FileStorage, WindowSettings}, *, }; +pub use egui::app::{App, Backend, RunMode, Storage}; + const EGUI_MEMORY_KEY: &str = "egui"; const WINDOW_KEY: &str = "window"; -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum RunMode { - /// Uses `request_animation_frame` to repaint the UI on each display Hz. - /// This is good for games and stuff where you want to run logic at e.g. 60 FPS. - Continuous, - - /// Only repaint when there are animations or input (mouse movement, keyboard input etc). - Reactive, -} - -pub trait App { - /// Called onced per frame for you to draw the UI. - fn ui(&mut self, ui: &mut egui::Ui, runner: &mut Runner); - - /// Called once on shutdown. Allows you to save state. - fn on_exit(&mut self, persistence: &mut Persistence); -} - -pub struct Runner { +pub struct GliumBackend { frame_times: egui::MovementTracker, quit: bool, run_mode: RunMode, } -impl Runner { +impl GliumBackend { pub fn new(run_mode: RunMode) -> Self { Self { frame_times: egui::MovementTracker::new(1000, 1.0), @@ -40,33 +24,35 @@ impl Runner { run_mode, } } +} - pub fn run_mode(&self) -> RunMode { +impl Backend for GliumBackend { + fn run_mode(&self) -> RunMode { self.run_mode } - pub fn set_run_mode(&mut self, run_mode: RunMode) { + fn set_run_mode(&mut self, run_mode: RunMode) { self.run_mode = run_mode; } - pub fn quit(&mut self) { - self.quit = true; - } - - pub fn cpu_time(&self) -> f32 { + fn cpu_time(&self) -> f32 { self.frame_times.average().unwrap_or_default() } - pub fn fps(&self) -> f32 { + fn fps(&self) -> f32 { 1.0 / self.frame_times.mean_time_interval().unwrap_or_default() } + + fn quit(&mut self) { + self.quit = true; + } } /// Run an egui app pub fn run( title: &str, run_mode: RunMode, - mut persistence: Persistence, + mut storage: FileStorage, mut app: impl App + 'static, ) -> ! { let event_loop = glutin::event_loop::EventLoop::new(); @@ -76,7 +62,7 @@ pub fn run( .with_title(title) .with_transparent(false); - let window_settings: Option = persistence.get_value(WINDOW_KEY); + let window_settings: Option = egui::app::get_value(&storage, WINDOW_KEY); if let Some(window_settings) = &window_settings { window = window_settings.initialize_size(window); } @@ -93,14 +79,14 @@ pub fn run( } let mut ctx = egui::Context::new(); - *ctx.memory() = persistence.get_value(EGUI_MEMORY_KEY).unwrap_or_default(); + *ctx.memory() = egui::app::get_value(&storage, EGUI_MEMORY_KEY).unwrap_or_default(); let mut painter = Painter::new(&display); let mut raw_input = make_raw_input(&display); // used to keep track of time for animations let start_time = Instant::now(); - let mut runner = Runner::new(run_mode); + let mut runner = GliumBackend::new(run_mode); let mut clipboard = init_clipboard(); event_loop.run(move |event, _, control_flow| { @@ -134,10 +120,14 @@ pub fn run( display.gl_window().window().request_redraw(); // TODO: maybe only on some events? } glutin::event::Event::LoopDestroyed => { - persistence.set_value(WINDOW_KEY, &WindowSettings::from_display(&display)); - persistence.set_value(EGUI_MEMORY_KEY, &*ctx.memory()); - app.on_exit(&mut persistence); - persistence.save(); + egui::app::set_value( + &mut storage, + WINDOW_KEY, + &WindowSettings::from_display(&display), + ); + egui::app::set_value(&mut storage, EGUI_MEMORY_KEY, &*ctx.memory()); + app.on_exit(&mut storage); + storage.save(); } _ => (), } diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index 93c50b55..371a9d56 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -3,12 +3,12 @@ #![allow(clippy::single_match)] #![allow(deprecated)] // TODO: remove +mod backend; mod painter; -pub mod persistence; -mod runner; +pub mod storage; +pub use backend::*; pub use painter::Painter; -pub use runner::*; use { clipboard::ClipboardProvider, diff --git a/egui_glium/src/persistence.rs b/egui_glium/src/storage.rs similarity index 89% rename from egui_glium/src/persistence.rs rename to egui_glium/src/storage.rs index 1df7d2ce..e23f07a0 100644 --- a/egui_glium/src/persistence.rs +++ b/egui_glium/src/storage.rs @@ -4,13 +4,13 @@ use std::collections::HashMap; /// A key-value store backed by a JSON file on disk. /// Used to restore egui state, glium window position/size and app state. -pub struct Persistence { +pub struct FileStorage { path: String, kv: HashMap, dirty: bool, } -impl Persistence { +impl FileStorage { pub fn from_path(path: String) -> Self { Self { kv: read_json(&path).unwrap_or_default(), @@ -19,23 +19,6 @@ impl Persistence { } } - pub fn get_value(&self, key: &str) -> Option { - self.kv - .get(key) - .and_then(|value| serde_json::from_str(value).ok()) - } - - pub fn set_string(&mut self, key: &str, value: String) { - if self.kv.get(key) != Some(&value) { - self.kv.insert(key.to_owned(), value); - self.dirty = true; - } - } - - pub fn set_value(&mut self, key: &str, value: &T) { - self.set_string(key, serde_json::to_string(value).unwrap()); - } - pub fn save(&mut self) { if self.dirty { serde_json::to_writer(std::fs::File::create(&self.path).unwrap(), &self.kv).unwrap(); @@ -44,6 +27,19 @@ impl Persistence { } } +impl egui::app::Storage for FileStorage { + fn get_string(&self, key: &str) -> Option<&str> { + self.kv.get(key).map(String::as_str) + } + + fn set_string(&mut self, key: &str, value: String) { + if self.kv.get(key) != Some(&value) { + self.kv.insert(key.to_owned(), value); + self.dirty = true; + } + } +} + // ---------------------------------------------------------------------------- pub fn read_json(memory_json_path: impl AsRef) -> Option @@ -69,7 +65,7 @@ where } // ---------------------------------------------------------------------------- -/// Alternative to `Persistence` +/// Alternative to `FileStorage` pub fn read_memory(ctx: &egui::Context, memory_json_path: impl AsRef) { let memory: Option = read_json(memory_json_path); if let Some(memory) = memory { @@ -77,7 +73,7 @@ pub fn read_memory(ctx: &egui::Context, memory_json_path: impl AsRef, diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs new file mode 100644 index 00000000..2f119d29 --- /dev/null +++ b/egui_web/src/backend.rs @@ -0,0 +1,184 @@ +use crate::*; + +pub use egui::app::{App, Backend, RunMode, WebInfo}; + +// ---------------------------------------------------------------------------- + +pub struct WebBackend { + ctx: Arc, + painter: webgl::Painter, + frame_times: egui::MovementTracker, + frame_start: Option, + run_mode: RunMode, + last_save_time: Option, +} + +impl WebBackend { + pub fn new(canvas_id: &str, run_mode: RunMode) -> Result { + let ctx = egui::Context::new(); + load_memory(&ctx); + Ok(Self { + ctx, + painter: webgl::Painter::new(canvas_id)?, + frame_times: egui::MovementTracker::new(1000, 1.0), + frame_start: None, + run_mode, + last_save_time: None, + }) + } + + /// id of the canvas html element containing the rendering + pub fn canvas_id(&self) -> &str { + self.painter.canvas_id() + } + + /// Returns a master fullscreen UI, covering the entire screen. + pub fn begin_frame(&mut self, raw_input: egui::RawInput) -> egui::Ui { + self.frame_start = Some(now_sec()); + self.ctx.begin_frame(raw_input) + } + + pub fn end_frame(&mut self) -> Result<(egui::Output, egui::PaintJobs), JsValue> { + let frame_start = self + .frame_start + .take() + .expect("unmatched calls to begin_frame/end_frame"); + + let (output, paint_jobs) = self.ctx.end_frame(); + + self.auto_save(); + + let now = now_sec(); + self.frame_times.add(now, (now - frame_start) as f32); + + Ok((output, paint_jobs)) + } + + pub fn paint(&mut self, paint_jobs: egui::PaintJobs) -> Result<(), JsValue> { + let bg_color = egui::color::TRANSPARENT; // Use background css color. + self.painter.paint_jobs( + bg_color, + paint_jobs, + self.ctx.texture(), + self.ctx.pixels_per_point(), + ) + } + + pub fn auto_save(&mut self) { + let now = now_sec(); + let time_since_last_save = now - self.last_save_time.unwrap_or(std::f64::NEG_INFINITY); + const AUTO_SAVE_INTERVAL: f64 = 5.0; + if time_since_last_save > AUTO_SAVE_INTERVAL { + self.last_save_time = Some(now); + save_memory(&self.ctx); + } + } + + pub fn painter_debug_info(&self) -> String { + self.painter.debug_info() + } +} + +impl Backend for WebBackend { + fn run_mode(&self) -> RunMode { + self.run_mode + } + + fn set_run_mode(&mut self, run_mode: RunMode) { + self.run_mode = run_mode; + } + + fn web_info(&self) -> Option { + Some(WebInfo { + web_location_hash: location_hash().unwrap_or_default(), + }) + } + + /// excludes painting + fn cpu_time(&self) -> f32 { + self.frame_times.average().unwrap_or_default() + } + + fn fps(&self) -> f32 { + 1.0 / self.frame_times.mean_time_interval().unwrap_or_default() + } +} + +// ---------------------------------------------------------------------------- + +// TODO: Just use RawInput? +/// Data gathered between frames. +/// Is translated to `egui::RawInput` at the start of each frame. +#[derive(Default)] +pub struct WebInput { + pub mouse_pos: Option, + pub mouse_down: bool, // TODO: which button + pub is_touch: bool, + pub scroll_delta: egui::Vec2, + pub events: Vec, +} + +impl WebInput { + pub fn new_frame(&mut self) -> egui::RawInput { + egui::RawInput { + mouse_down: self.mouse_down, + mouse_pos: self.mouse_pos, + scroll_delta: std::mem::take(&mut self.scroll_delta), + screen_size: screen_size().unwrap(), + pixels_per_point: Some(pixels_per_point()), + time: now_sec(), + seconds_since_midnight: Some(seconds_since_midnight()), + events: std::mem::take(&mut self.events), + } + } +} + +// ---------------------------------------------------------------------------- + +pub struct AppRunner { + pub web_backend: WebBackend, + pub web_input: WebInput, + pub app: Box, + pub needs_repaint: bool, // TODO: move +} + +impl AppRunner { + pub fn new(web_backend: WebBackend, app: Box) -> Result { + Ok(Self { + web_backend, + web_input: Default::default(), + app, + needs_repaint: true, // TODO: move + }) + } + + pub fn canvas_id(&self) -> &str { + self.web_backend.canvas_id() + } + + pub fn logic(&mut self) -> Result<(egui::Output, egui::PaintJobs), JsValue> { + resize_to_screen_size(self.web_backend.canvas_id()); + + let raw_input = self.web_input.new_frame(); + + let mut ui = self.web_backend.begin_frame(raw_input); + self.app.ui(&mut ui, &mut self.web_backend); + let (output, paint_jobs) = self.web_backend.end_frame()?; + handle_output(&output); + Ok((output, paint_jobs)) + } + + pub fn paint(&mut self, paint_jobs: egui::PaintJobs) -> Result<(), JsValue> { + self.web_backend.paint(paint_jobs) + } +} + +/// Install event listeners to register different input events +/// and starts running the given `AppRunner`. +pub fn run(app_runner: AppRunner) -> Result { + let runner_ref = AppRunnerRef(Arc::new(Mutex::new(app_runner))); + install_canvas_events(&runner_ref)?; + install_document_events(&runner_ref)?; + paint_and_schedule(runner_ref.clone())?; + Ok(runner_ref) +} diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index 62da5298..5485b43b 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -1,211 +1,15 @@ #![deny(warnings)] #![warn(clippy::all)] +pub mod backend; pub mod webgl; +pub use backend::*; + use parking_lot::Mutex; use std::sync::Arc; use wasm_bindgen::prelude::*; -// ---------------------------------------------------------------------------- - -pub struct WebInfo { - /// e.g. "#fragment" part of "www.example.com/index.html#fragment" - pub web_location_hash: String, -} - -/// Implement this and use `egui_web::AppRunner` to run your app. -pub trait App { - fn ui(&mut self, ui: &mut egui::Ui, backend: &mut Backend, info: &WebInfo); -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum RunMode { - /// Uses `request_animation_frame` to repaint the UI on each display Hz. - /// This is good for games and stuff where you want to run logic at e.g. 60 FPS. - Continuous, - - /// Only repaint when there are animations or input (mouse movement, keyboard input etc). - Reactive, -} - -// ---------------------------------------------------------------------------- - -pub struct Backend { - ctx: Arc, - painter: webgl::Painter, - frame_times: egui::MovementTracker, - frame_start: Option, - run_mode: RunMode, - last_save_time: Option, -} - -impl Backend { - pub fn new(canvas_id: &str, run_mode: RunMode) -> Result { - let ctx = egui::Context::new(); - load_memory(&ctx); - Ok(Backend { - ctx, - painter: webgl::Painter::new(canvas_id)?, - frame_times: egui::MovementTracker::new(1000, 1.0), - frame_start: None, - run_mode, - last_save_time: None, - }) - } - - pub fn run_mode(&self) -> RunMode { - self.run_mode - } - - pub fn set_run_mode(&mut self, run_mode: RunMode) { - self.run_mode = run_mode; - } - - /// id of the canvas html element containing the rendering - pub fn canvas_id(&self) -> &str { - self.painter.canvas_id() - } - - /// Returns a master fullscreen UI, covering the entire screen. - pub fn begin_frame(&mut self, raw_input: egui::RawInput) -> egui::Ui { - self.frame_start = Some(now_sec()); - self.ctx.begin_frame(raw_input) - } - - pub fn end_frame(&mut self) -> Result<(egui::Output, egui::PaintJobs), JsValue> { - let frame_start = self - .frame_start - .take() - .expect("unmatched calls to begin_frame/end_frame"); - - let (output, paint_jobs) = self.ctx.end_frame(); - - self.auto_save(); - - let now = now_sec(); - self.frame_times.add(now, (now - frame_start) as f32); - - Ok((output, paint_jobs)) - } - - pub fn paint(&mut self, paint_jobs: egui::PaintJobs) -> Result<(), JsValue> { - let bg_color = egui::color::TRANSPARENT; // Use background css color. - self.painter.paint_jobs( - bg_color, - paint_jobs, - self.ctx.texture(), - self.ctx.pixels_per_point(), - ) - } - - pub fn auto_save(&mut self) { - let now = now_sec(); - let time_since_last_save = now - self.last_save_time.unwrap_or(std::f64::NEG_INFINITY); - const AUTO_SAVE_INTERVAL: f64 = 5.0; - if time_since_last_save > AUTO_SAVE_INTERVAL { - self.last_save_time = Some(now); - save_memory(&self.ctx); - } - } - - pub fn painter_debug_info(&self) -> String { - self.painter.debug_info() - } - - /// excludes painting - pub fn cpu_time(&self) -> f32 { - self.frame_times.average().unwrap_or_default() - } - - pub fn fps(&self) -> f32 { - 1.0 / self.frame_times.mean_time_interval().unwrap_or_default() - } -} - -// ---------------------------------------------------------------------------- - -// TODO: Just use RawInput? -/// Data gathered between frames. -/// Is translated to `egui::RawInput` at the start of each frame. -#[derive(Default)] -pub struct WebInput { - pub mouse_pos: Option, - pub mouse_down: bool, // TODO: which button - pub is_touch: bool, - pub scroll_delta: egui::Vec2, - pub events: Vec, -} - -impl WebInput { - pub fn new_frame(&mut self) -> egui::RawInput { - egui::RawInput { - mouse_down: self.mouse_down, - mouse_pos: self.mouse_pos, - scroll_delta: std::mem::take(&mut self.scroll_delta), - screen_size: screen_size().unwrap(), - pixels_per_point: Some(pixels_per_point()), - time: now_sec(), - seconds_since_midnight: Some(seconds_since_midnight()), - events: std::mem::take(&mut self.events), - } - } -} - -// ---------------------------------------------------------------------------- - -pub struct AppRunner { - pub backend: Backend, - pub web_input: WebInput, - pub app: Box, - pub needs_repaint: bool, // TODO: move -} - -impl AppRunner { - pub fn new(backend: Backend, app: Box) -> Result { - Ok(Self { - backend, - web_input: Default::default(), - app, - needs_repaint: true, // TODO: move - }) - } - - pub fn canvas_id(&self) -> &str { - self.backend.canvas_id() - } - - pub fn logic(&mut self) -> Result<(egui::Output, egui::PaintJobs), JsValue> { - resize_to_screen_size(self.backend.canvas_id()); - - let raw_input = self.web_input.new_frame(); - - let info = WebInfo { - web_location_hash: location_hash().unwrap_or_default(), - }; - - let mut ui = self.backend.begin_frame(raw_input); - self.app.ui(&mut ui, &mut self.backend, &info); - let (output, paint_jobs) = self.backend.end_frame()?; - handle_output(&output); - Ok((output, paint_jobs)) - } - - pub fn paint(&mut self, paint_jobs: egui::PaintJobs) -> Result<(), JsValue> { - self.backend.paint(paint_jobs) - } -} - -/// Install event listeners to register different input events -/// and starts running the given `AppRunner`. -pub fn run(app_runner: AppRunner) -> Result { - let runner_ref = AppRunnerRef(Arc::new(Mutex::new(app_runner))); - install_canvas_events(&runner_ref)?; - install_document_events(&runner_ref)?; - paint_and_schedule(runner_ref.clone())?; - Ok(runner_ref) -} - // ---------------------------------------------------------------------------- // Helpers to hide some of the verbosity of web_sys @@ -412,7 +216,7 @@ pub struct AppRunnerRef(Arc>); fn paint_and_schedule(runner_ref: AppRunnerRef) -> Result<(), JsValue> { fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let mut runner_lock = runner_ref.0.lock(); - if runner_lock.backend.run_mode() == RunMode::Continuous || runner_lock.needs_repaint { + if runner_lock.web_backend.run_mode() == RunMode::Continuous || runner_lock.needs_repaint { runner_lock.needs_repaint = false; let (output, paint_jobs) = runner_lock.logic()?; runner_lock.paint(paint_jobs)?; diff --git a/example_glium/src/main.rs b/example_glium/src/main.rs index a1154e4d..78c9d35d 100644 --- a/example_glium/src/main.rs +++ b/example_glium/src/main.rs @@ -1,6 +1,4 @@ -use egui_glium::{persistence::Persistence, RunMode, Runner}; - -const APP_KEY: &str = "app"; +use egui_glium::{storage::FileStorage, RunMode}; /// We dervive Deserialize/Serialize so we can persist app state on shutdown. #[derive(Default, serde::Deserialize, serde::Serialize)] @@ -8,10 +6,10 @@ struct MyApp { counter: u64, } -impl egui_glium::App for MyApp { +impl egui::app::App for MyApp { /// This function will be called whenever the Ui needs to be shown, /// which may be many times per second. - fn ui(&mut self, ui: &mut egui::Ui, _: &mut Runner) { + fn ui(&mut self, ui: &mut egui::Ui, _: &mut dyn egui::app::Backend) { if ui.button("Increment").clicked { self.counter += 1; } @@ -21,14 +19,14 @@ impl egui_glium::App for MyApp { ui.label(format!("Counter: {}", self.counter)); } - fn on_exit(&mut self, persistence: &mut Persistence) { - persistence.set_value(APP_KEY, self); // Save app state + fn on_exit(&mut self, storage: &mut dyn egui::app::Storage) { + egui::app::set_value(storage, egui::app::APP_KEY, self); } } fn main() { let title = "My Egui Window"; - let persistence = Persistence::from_path(".egui_example_glium.json".into()); // Where to persist app state - let app: MyApp = persistence.get_value(APP_KEY).unwrap_or_default(); // Restore `MyApp` from file, or create new `MyApp`. - egui_glium::run(title, RunMode::Reactive, persistence, app); + let storage = FileStorage::from_path(".egui_example_glium.json".into()); // Where to persist app state + let app: MyApp = egui::app::get_value(&storage, egui::app::APP_KEY).unwrap_or_default(); // Restore `MyApp` from file, or create new `MyApp`. + egui_glium::run(title, RunMode::Reactive, storage, app); }