diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index e2ca1c93..034e6827 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -2,7 +2,9 @@ #![warn(clippy::all)] #![allow(clippy::single_match)] #![allow(deprecated)] // TODO: remove + mod painter; +pub mod persistence; pub use painter::Painter; @@ -212,45 +214,6 @@ pub fn init_clipboard() -> Option { // ---------------------------------------------------------------------------- -pub fn read_json(memory_json_path: impl AsRef) -> Option -where - T: serde::de::DeserializeOwned, -{ - match std::fs::File::open(memory_json_path) { - Ok(file) => { - let reader = std::io::BufReader::new(file); - match serde_json::from_reader(reader) { - Ok(value) => Some(value), - Err(err) => { - eprintln!("ERROR: Failed to parse json: {}", err); - None - } - } - } - Err(_err) => { - // File probably doesn't exist. That's fine. - None - } - } -} - -pub fn read_memory(ctx: &Context, memory_json_path: impl AsRef) { - let memory: Option = read_json(memory_json_path); - if let Some(memory) = memory { - *ctx.memory() = memory; - } -} - -pub fn write_memory( - ctx: &Context, - memory_json_path: impl AsRef, -) -> Result<(), Box> { - serde_json::to_writer_pretty(std::fs::File::create(memory_json_path)?, &*ctx.memory())?; - Ok(()) -} - -// ---------------------------------------------------------------------------- - /// Time of day as seconds since midnight. Used for clock in example app. pub fn local_time_of_day() -> f64 { use chrono::Timelike; @@ -258,82 +221,6 @@ pub fn local_time_of_day() -> f64 { time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64) } -// ---------------------------------------------------------------------------- - -#[derive(Default, serde::Deserialize, serde::Serialize)] -pub struct WindowSettings { - pos: Option, - size: Option, -} - -impl WindowSettings { - pub fn from_json_file( - settings_json_path: impl AsRef, - ) -> Option { - read_json(settings_json_path) - } - - pub fn from_display(display: &glium::Display) -> Self { - Self { - pos: display - .gl_window() - .window() - .outer_position() - .ok() - .map(|p| pos2(p.x as f32, p.y as f32)), - - size: Some(vec2( - display.gl_window().window().inner_size().width as f32, - display.gl_window().window().inner_size().height as f32, - )), - } - } - - pub fn initialize_size( - &self, - window: glutin::window::WindowBuilder, - ) -> glutin::window::WindowBuilder { - if let Some(size) = self.size { - window.with_inner_size(glutin::dpi::PhysicalSize { - width: size.x as f64, - height: size.y as f64, - }) - } else { - window - } - - // Not yet available in winit: https://github.com/rust-windowing/winit/issues/1190 - // if let Some(pos) = self.pos { - // *window = window.with_outer_pos(glutin::dpi::PhysicalPosition { - // x: pos.x as f64, - // y: pos.y as f64, - // }); - // } - } - - pub fn restore_positions(&self, display: &glium::Display) { - // not needed, done by `initialize_size` - // let size = self.size.unwrap_or_else(|| vec2(1024.0, 800.0)); - // display - // .gl_window() - // .window() - // .set_inner_size(glutin::dpi::PhysicalSize { - // width: size.x as f64, - // height: size.y as f64, - // }); - - if let Some(pos) = self.pos { - display - .gl_window() - .window() - .set_outer_position(glutin::dpi::PhysicalPosition::new( - pos.x as f64, - pos.y as f64, - )); - } - } -} - pub fn make_raw_input(display: &glium::Display) -> egui::RawInput { let pixels_per_point = display.gl_window().window().scale_factor() as f32; egui::RawInput { diff --git a/egui_glium/src/persistence.rs b/egui_glium/src/persistence.rs new file mode 100644 index 00000000..25a7b9b3 --- /dev/null +++ b/egui_glium/src/persistence.rs @@ -0,0 +1,165 @@ +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 { + path: String, + kv: HashMap, + dirty: bool, +} + +impl Persistence { + pub fn from_path(path: String) -> Self { + Self { + kv: read_json(&path).unwrap_or_default(), + path, + dirty: false, + } + } + + 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(); + self.dirty = false; + } + } +} + +// ---------------------------------------------------------------------------- + +pub fn read_json(memory_json_path: impl AsRef) -> Option +where + T: serde::de::DeserializeOwned, +{ + match std::fs::File::open(memory_json_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + match serde_json::from_reader(reader) { + Ok(value) => Some(value), + Err(err) => { + eprintln!("ERROR: Failed to parse json: {}", err); + None + } + } + } + Err(_err) => { + // File probably doesn't exist. That's fine. + None + } + } +} +// ---------------------------------------------------------------------------- + +/// Alternative to `Persistence` +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 { + *ctx.memory() = memory; + } +} + +/// Alternative to `Persistence` +pub fn write_memory( + ctx: &egui::Context, + memory_json_path: impl AsRef, +) -> Result<(), Box> { + serde_json::to_writer_pretty(std::fs::File::create(memory_json_path)?, &*ctx.memory())?; + Ok(()) +} + +// ---------------------------------------------------------------------------- + +use glium::glutin; + +#[derive(Default, serde::Deserialize, serde::Serialize)] +pub struct WindowSettings { + pos: Option, + size: Option, +} + +impl WindowSettings { + pub fn from_json_file( + settings_json_path: impl AsRef, + ) -> Option { + read_json(settings_json_path) + } + + pub fn from_display(display: &glium::Display) -> Self { + Self { + pos: display + .gl_window() + .window() + .outer_position() + .ok() + .map(|p| egui::pos2(p.x as f32, p.y as f32)), + + size: Some(egui::vec2( + display.gl_window().window().inner_size().width as f32, + display.gl_window().window().inner_size().height as f32, + )), + } + } + + pub fn initialize_size( + &self, + window: glutin::window::WindowBuilder, + ) -> glutin::window::WindowBuilder { + if let Some(size) = self.size { + window.with_inner_size(glutin::dpi::PhysicalSize { + width: size.x as f64, + height: size.y as f64, + }) + } else { + window + } + + // Not yet available in winit: https://github.com/rust-windowing/winit/issues/1190 + // if let Some(pos) = self.pos { + // *window = window.with_outer_pos(glutin::dpi::PhysicalPosition { + // x: pos.x as f64, + // y: pos.y as f64, + // }); + // } + } + + pub fn restore_positions(&self, display: &glium::Display) { + // not needed, done by `initialize_size` + // let size = self.size.unwrap_or_else(|| vec2(1024.0, 800.0)); + // display + // .gl_window() + // .window() + // .set_inner_size(glutin::dpi::PhysicalSize { + // width: size.x as f64, + // height: size.y as f64, + // }); + + if let Some(pos) = self.pos { + display + .gl_window() + .window() + .set_outer_position(glutin::dpi::PhysicalPosition::new( + pos.x as f64, + pos.y as f64, + )); + } + } +} diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index 3c763497..62da5298 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -36,8 +36,6 @@ pub struct Backend { painter: webgl::Painter, frame_times: egui::MovementTracker, frame_start: Option, - /// If true, paint at full framerate always. - /// If false, only paint on input. run_mode: RunMode, last_save_time: Option, } diff --git a/example_glium/src/main.rs b/example_glium/src/main.rs index f2feb7a7..b43e8706 100644 --- a/example_glium/src/main.rs +++ b/example_glium/src/main.rs @@ -4,18 +4,76 @@ use std::time::Instant; use { - egui::examples::ExampleApp, - egui_glium::{make_raw_input, read_json, WindowSettings}, + egui_glium::{ + make_raw_input, + persistence::{Persistence, WindowSettings}, + }, glium::glutin, }; -fn main() { - // TODO: combine into one json file? - let memory_path = "egui.json"; - let settings_json_path: &str = "window.json"; - let app_json_path: &str = "egui_example_app.json"; +#[derive(Default, serde::Deserialize, serde::Serialize)] +struct App { + egui_example_app: egui::ExampleApp, +} - let mut egui_example_app: ExampleApp = read_json(app_json_path).unwrap_or_default(); +impl App { + pub fn ui(&mut self, ui: &mut egui::Ui, runner: &mut Runner) { + self.egui_example_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 quit inside of Glium").text_style(TextStyle::Heading)); + if ui.add(Button::new("Quit")).clicked { + runner.quit(); + return; + } + + ui.add( + label!( + "CPU usage: {:.2} ms (excludes painting)", + 1e3 * runner.cpu_usage() + ) + .text_style(TextStyle::Monospace), + ); + ui.add(label!("FPS: {:.1}", runner.fps()).text_style(TextStyle::Monospace)); + } +} + +struct Runner { + frame_times: egui::MovementTracker, + quit: bool, +} + +impl Runner { + pub fn new() -> Self { + Self { + frame_times: egui::MovementTracker::new(1000, 1.0), + quit: false, + } + } + + pub fn quit(&mut self) { + self.quit = true; + } + + pub fn cpu_usage(&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() + } +} + +fn main() { + const EGUI_MEMORY_KEY: &str = "egui"; + const WINDOW_KEY: &str = "window"; + const APP_KEY: &str = "app"; + + let mut persistence = Persistence::from_path("egui_example_glium.json".into()); + + let mut app: App = persistence.get_value("app").unwrap_or_default(); let event_loop = glutin::event_loop::EventLoop::new(); let mut window = glutin::window::WindowBuilder::new() @@ -24,7 +82,7 @@ fn main() { .with_title("Egui glium example") .with_transparent(false); - let window_settings = WindowSettings::from_json_file(settings_json_path); + let window_settings: Option = persistence.get_value(WINDOW_KEY); if let Some(window_settings) = &window_settings { window = window_settings.initialize_size(window); } @@ -41,16 +99,16 @@ fn main() { } let mut ctx = egui::Context::new(); + *ctx.memory() = persistence.get_value(EGUI_MEMORY_KEY).unwrap_or_default(); + let mut painter = egui_glium::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 frame_times = egui::MovementTracker::new(1000, 1.0); + let mut runner = Runner::new(); let mut clipboard = egui_glium::init_clipboard(); - egui_glium::read_memory(&ctx, memory_path); - event_loop.run(move |event, _, control_flow| { *control_flow = glutin::event_loop::ControlFlow::Wait; @@ -61,35 +119,10 @@ fn main() { raw_input.seconds_since_midnight = Some(egui_glium::local_time_of_day()); let mut ui = ctx.begin_frame(raw_input.take()); - egui_example_app.ui(&mut 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 running inside of Glium").text_style(TextStyle::Heading)); - if ui.add(Button::new("Quit")).clicked { - *control_flow = glutin::event_loop::ControlFlow::Exit; - } - - ui.add( - label!( - "CPU usage: {:.2} ms (excludes painting)", - 1e3 * frame_times.average().unwrap_or_default() - ) - .text_style(TextStyle::Monospace), - ); - ui.add( - label!( - "FPS: {:.1}", - 1.0 / frame_times.mean_time_interval().unwrap_or_default() - ) - .text_style(TextStyle::Monospace), - ); - } - + app.ui(&mut ui, &mut runner); let (output, paint_jobs) = ctx.end_frame(); - frame_times.add( + runner.frame_times.add( raw_input.time, (Instant::now() - egui_start).as_secs_f64() as f32, ); @@ -97,28 +130,20 @@ fn main() { painter.paint_jobs(&display, paint_jobs, ctx.texture()); egui_glium::handle_output(output, &display, clipboard.as_mut()); - display.gl_window().window().request_redraw(); // TODO: only if needed (new events etc) + if runner.quit { + *control_flow = glutin::event_loop::ControlFlow::Exit + } else { + display.gl_window().window().request_redraw(); // TODO: only if needed (new events etc) + } } glutin::event::Event::WindowEvent { event, .. } => { egui_glium::input_to_egui(event, clipboard.as_mut(), &mut raw_input, control_flow); } glutin::event::Event::LoopDestroyed => { - // Save state to disk: - if let Err(err) = egui_glium::write_memory(&ctx, memory_path) { - eprintln!("ERROR: Failed to save egui state: {}", err); - } - - serde_json::to_writer_pretty( - std::fs::File::create(app_json_path).unwrap(), - &egui_example_app, - ) - .unwrap(); - - serde_json::to_writer_pretty( - std::fs::File::create(settings_json_path).unwrap(), - &WindowSettings::from_display(&display), - ) - .unwrap(); + persistence.set_value(APP_KEY, &app); + persistence.set_value(WINDOW_KEY, &WindowSettings::from_display(&display)); + persistence.set_value(EGUI_MEMORY_KEY, &*ctx.memory()); + persistence.save(); } _ => (), }