diff --git a/demo_glium/Cargo.toml b/demo_glium/Cargo.toml index bdb6be39..8e9fd894 100644 --- a/demo_glium/Cargo.toml +++ b/demo_glium/Cargo.toml @@ -6,6 +6,6 @@ license = "MIT OR Apache-2.0" edition = "2018" [dependencies] -egui = { path = "../egui", features = ["serde"] } +egui = { path = "../egui", features = ["serde", "serde_json"] } egui_glium = { path = "../egui_glium" } serde = { version = "1", features = ["derive"] } diff --git a/demo_web/Cargo.toml b/demo_web/Cargo.toml index 05cb684a..1e0fcb05 100644 --- a/demo_web/Cargo.toml +++ b/demo_web/Cargo.toml @@ -9,7 +9,7 @@ edition = "2018" crate-type = ["cdylib", "rlib"] [dependencies] -egui = { path = "../egui", features = ["serde"] } +egui = { path = "../egui", features = ["serde", "serde_json"] } egui_web = { path = "../egui_web" } js-sys = "0.3" serde = { version = "1", features = ["derive"] } diff --git a/egui/src/app.rs b/egui/src/app.rs index 545fdd44..4a4017e3 100644 --- a/egui/src/app.rs +++ b/egui/src/app.rs @@ -19,6 +19,20 @@ pub trait App { crate::Srgba::from_rgb(16, 16, 16).into() } + /// Called once on start. Allows you to restore state. + fn load(&mut self, _storage: &dyn Storage) {} + + /// Called on shutdown, and perhaps at regular intervals. Allows you to save state. + fn save(&mut self, _storage: &mut dyn Storage) {} + + /// 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. @@ -27,9 +41,6 @@ pub trait App { /// 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: &crate::CtxRef, integration_context: &mut IntegrationContext<'_>); - - /// Called once on shutdown. Allows you to save state. - fn on_exit(&mut self, _storage: &mut dyn Storage) {} } pub struct IntegrationContext<'a> { diff --git a/egui/src/demos/app.rs b/egui/src/demos/app.rs index 4299b8cf..4d83feca 100644 --- a/egui/src/demos/app.rs +++ b/egui/src/demos/app.rs @@ -281,6 +281,16 @@ impl app::App for DemoApp { "Egui Demo" } + #[cfg(feature = "serde_json")] + fn load(&mut self, storage: &dyn crate::app::Storage) { + *self = crate::app::get_value(storage, crate::app::APP_KEY).unwrap_or_default() + } + + #[cfg(feature = "serde_json")] + fn save(&mut self, storage: &mut dyn crate::app::Storage) { + crate::app::set_value(storage, crate::app::APP_KEY, self); + } + fn ui(&mut self, ctx: &CtxRef, integration_context: &mut crate::app::IntegrationContext<'_>) { self.frame_history .on_new_frame(ctx.input().time, integration_context.info.cpu_usage); @@ -339,9 +349,4 @@ impl app::App for DemoApp { ctx.request_repaint(); } } - - #[cfg(feature = "serde_json")] - fn on_exit(&mut self, storage: &mut dyn app::Storage) { - app::set_value(storage, app::APP_KEY, self); - } } diff --git a/egui_glium/src/backend.rs b/egui_glium/src/backend.rs index 20d1805a..69a3caa6 100644 --- a/egui_glium/src/backend.rs +++ b/egui_glium/src/backend.rs @@ -65,6 +65,7 @@ fn create_display( /// Run an egui app pub fn run(mut storage: Box, mut app: Box) -> ! { + app.load(storage.as_ref()); let window_settings: Option = egui::app::get_value(storage.as_ref(), WINDOW_KEY); let event_loop = glutin::event_loop::EventLoop::with_user_event(); @@ -85,7 +86,7 @@ pub fn run(mut storage: Box, mut app: Box) -> ! event_loop.run(move |event, _, control_flow| { let mut redraw = || { - let egui_start = Instant::now(); + let frame_start = Instant::now(); input_state.raw.time = Some(start_time.elapsed().as_nanos() as f64 * 1e-9); input_state.raw.screen_rect = Some(Rect::from_min_size( Default::default(), @@ -109,7 +110,7 @@ pub fn run(mut storage: Box, mut app: Box) -> ! let (egui_output, paint_commands) = ctx.end_frame(); let paint_jobs = ctx.tesselate(paint_commands); - let frame_time = (Instant::now() - egui_start).as_secs_f64() as f32; + let frame_time = (Instant::now() - frame_start).as_secs_f64() as f32; previous_frame_time = Some(frame_time); painter.paint_jobs( &display, @@ -172,7 +173,8 @@ pub fn run(mut storage: Box, mut app: Box) -> ! &WindowSettings::from_display(&display), ); egui::app::set_value(storage.as_mut(), EGUI_MEMORY_KEY, &*ctx.memory()); - app.on_exit(storage.as_mut()); + app.save(storage.as_mut()); + app.on_exit(); storage.flush(); } diff --git a/egui_web/CHANGELOG.md b/egui_web/CHANGELOG.md index 78c9faa1..ec7269d3 100644 --- a/egui_web/CHANGELOG.md +++ b/egui_web/CHANGELOG.md @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased +### Added ⭐ + +* Auto-save of app state to local storage + ### Changed ⭐ * Set a maximum canvas size to alleviate performance issues on some machines diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs index 65070d32..c368d1c7 100644 --- a/egui_web/src/backend.rs +++ b/egui_web/src/backend.rs @@ -12,19 +12,16 @@ pub struct WebBackend { painter: webgl::Painter, previous_frame_time: Option, frame_start: Option, - last_save_time: Option, } impl WebBackend { pub fn new(canvas_id: &str) -> Result { let ctx = egui::CtxRef::default(); - load_memory(&ctx); Ok(Self { ctx, painter: webgl::Painter::new(canvas_id)?, previous_frame_time: None, frame_start: None, - last_save_time: None, }) } @@ -47,8 +44,6 @@ impl WebBackend { let (output, paint_commands) = self.ctx.end_frame(); let paint_jobs = self.ctx.tesselate(paint_commands); - self.auto_save(); - let now = now_sec(); self.previous_frame_time = Some((now - frame_start) as f32); @@ -68,16 +63,6 @@ impl WebBackend { ) } - 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() } @@ -159,19 +144,37 @@ pub struct AppRunner { pub input: WebInput, pub app: Box, pub needs_repaint: std::sync::Arc, + pub storage: LocalStorage, + pub last_save_time: f64, } impl AppRunner { pub fn new(web_backend: WebBackend, mut app: Box) -> Result { + load_memory(&web_backend.ctx); + let storage = LocalStorage::default(); + app.load(&storage); app.setup(&web_backend.ctx); Ok(Self { web_backend, input: Default::default(), app, needs_repaint: Default::default(), + storage, + last_save_time: now_sec(), }) } + pub fn auto_save(&mut self) { + let now = now_sec(); + let time_since_last_save = now - self.last_save_time; + + if time_since_last_save > self.app.auto_save_interval().as_secs_f64() { + save_memory(&self.web_backend.ctx); + self.app.save(&mut self.storage); + self.last_save_time = now; + } + } + pub fn canvas_id(&self) -> &str { self.web_backend.canvas_id() } diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index 7fa5bd6a..73dcc46b 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -152,7 +152,7 @@ pub fn load_memory(ctx: &egui::Context) { *ctx.memory() = memory; } Err(err) => { - console_log(format!("ERROR: Failed to parse memory json: {}", err)); + console_error(format!("Failed to parse memory json: {}", err)); } } } @@ -164,14 +164,12 @@ pub fn save_memory(ctx: &egui::Context) { local_storage_set("egui_memory_json", &json); } Err(err) => { - console_log(format!( - "ERROR: Failed to serialize memory as json: {}", - err - )); + console_error(format!("Failed to serialize memory as json: {}", err)); } } } +#[derive(Default)] pub struct LocalStorage {} impl egui::app::Storage for LocalStorage { @@ -220,7 +218,7 @@ pub fn set_clipboard_text(s: &str) { let future = wasm_bindgen_futures::JsFuture::from(promise); let future = async move { if let Err(err) = future.await { - console_log(format!("Copy/cut action denied: {:?}", err)); + console_error(format!("Copy/cut action denied: {:?}", err)); } }; wasm_bindgen_futures::spawn_local(future); @@ -341,7 +339,9 @@ fn paint_and_schedule(runner_ref: AppRunnerRef) -> Result<(), JsValue> { if output.needs_repaint { runner_lock.needs_repaint.set_true(); } + runner_lock.auto_save(); } + Ok(()) }