[egui_web] Auto-save app state to Local Storage every 30 seconds
This commit is contained in:
parent
2fe1e99218
commit
89937bf636
8 changed files with 59 additions and 34 deletions
|
@ -6,6 +6,6 @@ license = "MIT OR Apache-2.0"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
egui = { path = "../egui", features = ["serde"] }
|
egui = { path = "../egui", features = ["serde", "serde_json"] }
|
||||||
egui_glium = { path = "../egui_glium" }
|
egui_glium = { path = "../egui_glium" }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
|
@ -9,7 +9,7 @@ edition = "2018"
|
||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
egui = { path = "../egui", features = ["serde"] }
|
egui = { path = "../egui", features = ["serde", "serde_json"] }
|
||||||
egui_web = { path = "../egui_web" }
|
egui_web = { path = "../egui_web" }
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
|
@ -19,6 +19,20 @@ pub trait App {
|
||||||
crate::Srgba::from_rgb(16, 16, 16).into()
|
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.
|
/// Called once before the first frame.
|
||||||
/// Allows you to do setup code and to call `ctx.set_fonts()`.
|
/// Allows you to do setup code and to call `ctx.set_fonts()`.
|
||||||
/// Optional.
|
/// Optional.
|
||||||
|
@ -27,9 +41,6 @@ pub trait App {
|
||||||
/// Called each time the UI needs repainting, which may be many times per second.
|
/// 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`.
|
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
|
||||||
fn ui(&mut self, ctx: &crate::CtxRef, integration_context: &mut IntegrationContext<'_>);
|
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> {
|
pub struct IntegrationContext<'a> {
|
||||||
|
|
|
@ -281,6 +281,16 @@ impl app::App for DemoApp {
|
||||||
"Egui Demo"
|
"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<'_>) {
|
fn ui(&mut self, ctx: &CtxRef, integration_context: &mut crate::app::IntegrationContext<'_>) {
|
||||||
self.frame_history
|
self.frame_history
|
||||||
.on_new_frame(ctx.input().time, integration_context.info.cpu_usage);
|
.on_new_frame(ctx.input().time, integration_context.info.cpu_usage);
|
||||||
|
@ -339,9 +349,4 @@ impl app::App for DemoApp {
|
||||||
ctx.request_repaint();
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,7 @@ fn create_display(
|
||||||
|
|
||||||
/// Run an egui app
|
/// Run an egui app
|
||||||
pub fn run(mut storage: Box<dyn egui::app::Storage>, mut app: Box<dyn App>) -> ! {
|
pub fn run(mut storage: Box<dyn egui::app::Storage>, mut app: Box<dyn App>) -> ! {
|
||||||
|
app.load(storage.as_ref());
|
||||||
let window_settings: Option<WindowSettings> =
|
let window_settings: Option<WindowSettings> =
|
||||||
egui::app::get_value(storage.as_ref(), WINDOW_KEY);
|
egui::app::get_value(storage.as_ref(), WINDOW_KEY);
|
||||||
let event_loop = glutin::event_loop::EventLoop::with_user_event();
|
let event_loop = glutin::event_loop::EventLoop::with_user_event();
|
||||||
|
@ -85,7 +86,7 @@ pub fn run(mut storage: Box<dyn egui::app::Storage>, mut app: Box<dyn App>) -> !
|
||||||
|
|
||||||
event_loop.run(move |event, _, control_flow| {
|
event_loop.run(move |event, _, control_flow| {
|
||||||
let mut redraw = || {
|
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.time = Some(start_time.elapsed().as_nanos() as f64 * 1e-9);
|
||||||
input_state.raw.screen_rect = Some(Rect::from_min_size(
|
input_state.raw.screen_rect = Some(Rect::from_min_size(
|
||||||
Default::default(),
|
Default::default(),
|
||||||
|
@ -109,7 +110,7 @@ pub fn run(mut storage: Box<dyn egui::app::Storage>, mut app: Box<dyn App>) -> !
|
||||||
let (egui_output, paint_commands) = ctx.end_frame();
|
let (egui_output, paint_commands) = ctx.end_frame();
|
||||||
let paint_jobs = ctx.tesselate(paint_commands);
|
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);
|
previous_frame_time = Some(frame_time);
|
||||||
painter.paint_jobs(
|
painter.paint_jobs(
|
||||||
&display,
|
&display,
|
||||||
|
@ -172,7 +173,8 @@ pub fn run(mut storage: Box<dyn egui::app::Storage>, mut app: Box<dyn App>) -> !
|
||||||
&WindowSettings::from_display(&display),
|
&WindowSettings::from_display(&display),
|
||||||
);
|
);
|
||||||
egui::app::set_value(storage.as_mut(), EGUI_MEMORY_KEY, &*ctx.memory());
|
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();
|
storage.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added ⭐
|
||||||
|
|
||||||
|
* Auto-save of app state to local storage
|
||||||
|
|
||||||
### Changed ⭐
|
### Changed ⭐
|
||||||
|
|
||||||
* Set a maximum canvas size to alleviate performance issues on some machines
|
* Set a maximum canvas size to alleviate performance issues on some machines
|
||||||
|
|
|
@ -12,19 +12,16 @@ pub struct WebBackend {
|
||||||
painter: webgl::Painter,
|
painter: webgl::Painter,
|
||||||
previous_frame_time: Option<f32>,
|
previous_frame_time: Option<f32>,
|
||||||
frame_start: Option<f64>,
|
frame_start: Option<f64>,
|
||||||
last_save_time: Option<f64>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebBackend {
|
impl WebBackend {
|
||||||
pub fn new(canvas_id: &str) -> Result<Self, JsValue> {
|
pub fn new(canvas_id: &str) -> Result<Self, JsValue> {
|
||||||
let ctx = egui::CtxRef::default();
|
let ctx = egui::CtxRef::default();
|
||||||
load_memory(&ctx);
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
ctx,
|
ctx,
|
||||||
painter: webgl::Painter::new(canvas_id)?,
|
painter: webgl::Painter::new(canvas_id)?,
|
||||||
previous_frame_time: None,
|
previous_frame_time: None,
|
||||||
frame_start: None,
|
frame_start: None,
|
||||||
last_save_time: None,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,8 +44,6 @@ impl WebBackend {
|
||||||
let (output, paint_commands) = self.ctx.end_frame();
|
let (output, paint_commands) = self.ctx.end_frame();
|
||||||
let paint_jobs = self.ctx.tesselate(paint_commands);
|
let paint_jobs = self.ctx.tesselate(paint_commands);
|
||||||
|
|
||||||
self.auto_save();
|
|
||||||
|
|
||||||
let now = now_sec();
|
let now = now_sec();
|
||||||
self.previous_frame_time = Some((now - frame_start) as f32);
|
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 {
|
pub fn painter_debug_info(&self) -> String {
|
||||||
self.painter.debug_info()
|
self.painter.debug_info()
|
||||||
}
|
}
|
||||||
|
@ -159,19 +144,37 @@ pub struct AppRunner {
|
||||||
pub input: WebInput,
|
pub input: WebInput,
|
||||||
pub app: Box<dyn App>,
|
pub app: Box<dyn App>,
|
||||||
pub needs_repaint: std::sync::Arc<NeedRepaint>,
|
pub needs_repaint: std::sync::Arc<NeedRepaint>,
|
||||||
|
pub storage: LocalStorage,
|
||||||
|
pub last_save_time: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppRunner {
|
impl AppRunner {
|
||||||
pub fn new(web_backend: WebBackend, mut app: Box<dyn App>) -> Result<Self, JsValue> {
|
pub fn new(web_backend: WebBackend, mut app: Box<dyn App>) -> Result<Self, JsValue> {
|
||||||
|
load_memory(&web_backend.ctx);
|
||||||
|
let storage = LocalStorage::default();
|
||||||
|
app.load(&storage);
|
||||||
app.setup(&web_backend.ctx);
|
app.setup(&web_backend.ctx);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
web_backend,
|
web_backend,
|
||||||
input: Default::default(),
|
input: Default::default(),
|
||||||
app,
|
app,
|
||||||
needs_repaint: Default::default(),
|
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 {
|
pub fn canvas_id(&self) -> &str {
|
||||||
self.web_backend.canvas_id()
|
self.web_backend.canvas_id()
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,7 +152,7 @@ pub fn load_memory(ctx: &egui::Context) {
|
||||||
*ctx.memory() = memory;
|
*ctx.memory() = memory;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
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);
|
local_storage_set("egui_memory_json", &json);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
console_log(format!(
|
console_error(format!("Failed to serialize memory as json: {}", err));
|
||||||
"ERROR: Failed to serialize memory as json: {}",
|
|
||||||
err
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct LocalStorage {}
|
pub struct LocalStorage {}
|
||||||
|
|
||||||
impl egui::app::Storage for 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 = wasm_bindgen_futures::JsFuture::from(promise);
|
||||||
let future = async move {
|
let future = async move {
|
||||||
if let Err(err) = future.await {
|
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);
|
wasm_bindgen_futures::spawn_local(future);
|
||||||
|
@ -341,7 +339,9 @@ fn paint_and_schedule(runner_ref: AppRunnerRef) -> Result<(), JsValue> {
|
||||||
if output.needs_repaint {
|
if output.needs_repaint {
|
||||||
runner_lock.needs_repaint.set_true();
|
runner_lock.needs_repaint.set_true();
|
||||||
}
|
}
|
||||||
|
runner_lock.auto_save();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue