From 5856cded95a17801e398cb9f3b10a117fc99152f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 16 Sep 2020 08:03:40 +0200 Subject: [PATCH] refactor RunMode: move it from backend to the demo App (#23) This simplifies the egui_glium and egui_web backends substantially, reduces the scope of RunMode to a single file, and removes duplicated code. Basically: this is how I should have written it from the beginning. --- demo_glium/src/main.rs | 4 +-- demo_web/src/lib.rs | 2 +- egui/src/app.rs | 17 ----------- egui/src/demos/app.rs | 59 +++++++++++++++++++++++++++++++++++---- egui_glium/src/backend.rs | 28 ++++--------------- egui_web/src/backend.rs | 14 ++-------- egui_web/src/lib.rs | 2 +- example_glium/src/main.rs | 4 +-- 8 files changed, 67 insertions(+), 63 deletions(-) diff --git a/demo_glium/src/main.rs b/demo_glium/src/main.rs index b6d2474f..ad1631e0 100644 --- a/demo_glium/src/main.rs +++ b/demo_glium/src/main.rs @@ -1,11 +1,11 @@ #![deny(warnings)] #![warn(clippy::all)] -use egui_glium::{storage::FileStorage, RunMode}; +use egui_glium::storage::FileStorage; fn main() { let title = "Egui glium demo"; 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); + egui_glium::run(title, storage, app); } diff --git a/demo_web/src/lib.rs b/demo_web/src/lib.rs index 61498fb9..0ce885d7 100644 --- a/demo_web/src/lib.rs +++ b/demo_web/src/lib.rs @@ -6,7 +6,7 @@ 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::WebBackend::new(canvas_id, egui_web::RunMode::Reactive)?; + let backend = egui_web::WebBackend::new(canvas_id)?; let app = Box::new(egui::DemoApp::default()); let runner = egui_web::AppRunner::new(backend, app)?; egui_web::run(runner)?; diff --git a/egui/src/app.rs b/egui/src/app.rs index 2d01250a..22817f25 100644 --- a/egui/src/app.rs +++ b/egui/src/app.rs @@ -19,29 +19,12 @@ pub trait App { 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 { - /// Repaint 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 diff --git a/egui/src/demos/app.rs b/egui/src/demos/app.rs index c9745276..c87d86e3 100644 --- a/egui/src/demos/app.rs +++ b/egui/src/demos/app.rs @@ -4,6 +4,49 @@ use crate::{app, color::*, containers::*, demos::*, paint::*, widgets::*, *}; // ---------------------------------------------------------------------------- +/// How often we repaint the demo app by default +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum RunMode { + /// This is the default for the demo. + /// + /// If this is selected, Egui is only updated if are input events + /// (like mouse movements) or there are some animations in the GUI. + /// + /// Reactive mode saves CPU. + /// + /// The downside is that the UI can become out-of-date if something it is supposed to monitor changes. + /// For instance, a GUI for a thermostat need to repaint each time the temperature changes. + /// To ensure the UI is up to date you need to call `egui::Context::request_repaint()` each + /// time such an event happens. You can also chose to call `request_repaint()` once every second + /// or after every single frame - this is called `Continuous` mode, + /// and for games and interactive tools that need repainting every frame anyway, this should be the default. + Reactive, + + /// This will call `egui::Context::request_repaint()` at the end of each frame + /// to request the backend to repaint as soon as possible. + /// + /// On most platforms this will mean that Egui will run at the display refresh rate of e.g. 60 Hz. + /// + /// For this demo it is not any reason to do so except to + /// demonstrate how quickly Egui runs. + /// + /// For games or other interactive apps, this is probably what you want to do. + /// It will guarantee that Egui is always up-to-date. + Continuous, +} + +/// Default for demo is Reactive since +/// 1) We want to use minimal CPU +/// 2) There are no external events that could invalidate the UI +/// so there are no events to miss. +impl Default for RunMode { + fn default() -> Self { + RunMode::Reactive + } +} + +// ---------------------------------------------------------------------------- + /// Demonstrates how to make an app using Egui. /// /// Implements `egui::app::App` so it can be used with @@ -12,6 +55,8 @@ use crate::{app, color::*, containers::*, demos::*, paint::*, widgets::*, *}; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct DemoApp { + #[cfg_attr(feature = "serde", serde(skip))] // go back to `Reactive` mode each time we start + run_mode: RunMode, previous_web_location_hash: String, open_windows: OpenWindows, demo_window: DemoWindow, @@ -172,16 +217,15 @@ impl DemoApp { ui.separator(); ui.horizontal(|ui| { - let mut run_mode = backend.run_mode(); + let run_mode = &mut self.run_mode; ui.label("Run mode:"); - ui.radio_value("Continuous", &mut run_mode, app::RunMode::Continuous) + ui.radio_value("Continuous", run_mode, RunMode::Continuous) .tooltip_text("Repaint everything each frame"); - ui.radio_value("Reactive", &mut run_mode, app::RunMode::Reactive) + ui.radio_value("Reactive", run_mode, 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 { + if self.run_mode == RunMode::Continuous { ui.add( label!("Repainting the UI each frame. FPS: {:.1}", backend.fps()) .text_style(TextStyle::Monospace), @@ -232,6 +276,11 @@ impl app::App for DemoApp { .map(|info| info.web_location_hash.as_str()) .unwrap_or_default(); self.ui(ui, web_location_hash); + + if self.run_mode == RunMode::Continuous { + // Tell the backend to repaint as soon as possible + ui.ctx().request_repaint(); + } } #[cfg(feature = "serde_json")] diff --git a/egui_glium/src/backend.rs b/egui_glium/src/backend.rs index e1128636..1912cbfd 100644 --- a/egui_glium/src/backend.rs +++ b/egui_glium/src/backend.rs @@ -6,7 +6,7 @@ use crate::{ }; pub use egui::{ - app::{App, Backend, RunMode, Storage}, + app::{App, Backend, Storage}, Srgba, }; @@ -16,30 +16,20 @@ const WINDOW_KEY: &str = "window"; pub struct GliumBackend { frame_times: egui::MovementTracker, quit: bool, - run_mode: RunMode, painter: Painter, } impl GliumBackend { - pub fn new(run_mode: RunMode, painter: Painter) -> Self { + pub fn new(painter: Painter) -> Self { Self { frame_times: egui::MovementTracker::new(1000, 1.0), quit: false, - run_mode, painter, } } } impl Backend for GliumBackend { - fn run_mode(&self) -> RunMode { - self.run_mode - } - - fn set_run_mode(&mut self, run_mode: RunMode) { - self.run_mode = run_mode; - } - fn cpu_time(&self) -> f32 { self.frame_times.average().unwrap_or_default() } @@ -62,12 +52,7 @@ impl Backend for GliumBackend { } /// Run an egui app -pub fn run( - title: &str, - run_mode: RunMode, - mut storage: FileStorage, - mut app: impl App + 'static, -) -> ! { +pub fn run(title: &str, mut storage: FileStorage, mut app: impl App + 'static) -> ! { let event_loop = glutin::event_loop::EventLoop::new(); let mut window = glutin::window::WindowBuilder::new() .with_decorations(true) @@ -98,7 +83,7 @@ pub fn run( // used to keep track of time for animations let start_time = Instant::now(); - let mut runner = GliumBackend::new(run_mode, Painter::new(&display)); + let mut runner = GliumBackend::new(Painter::new(&display)); let mut clipboard = init_clipboard(); event_loop.run(move |event, _, control_flow| { @@ -120,13 +105,10 @@ pub fn run( *control_flow = if runner.quit { glutin::event_loop::ControlFlow::Exit - } else if runner.run_mode() == RunMode::Continuous { + } else if output.needs_repaint { display.gl_window().window().request_redraw(); glutin::event_loop::ControlFlow::Poll } else { - if output.needs_repaint { - display.gl_window().window().request_redraw(); - } glutin::event_loop::ControlFlow::Wait }; diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs index 44ef32cb..c79520a9 100644 --- a/egui_web/src/backend.rs +++ b/egui_web/src/backend.rs @@ -1,7 +1,7 @@ use crate::*; pub use egui::{ - app::{App, Backend, RunMode, WebInfo}, + app::{App, Backend, WebInfo}, Srgba, }; @@ -12,12 +12,11 @@ pub struct WebBackend { 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 { + pub fn new(canvas_id: &str) -> Result { let ctx = egui::Context::new(); load_memory(&ctx); Ok(Self { @@ -25,7 +24,6 @@ impl WebBackend { painter: webgl::Painter::new(canvas_id)?, frame_times: egui::MovementTracker::new(1000, 1.0), frame_start: None, - run_mode, last_save_time: None, }) } @@ -83,14 +81,6 @@ impl WebBackend { } 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(), diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index df5be715..3c7048d5 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -229,7 +229,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.web_backend.run_mode() == RunMode::Continuous || runner_lock.needs_repaint { + if 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 e2efc1fd..e9eee1b2 100644 --- a/example_glium/src/main.rs +++ b/example_glium/src/main.rs @@ -4,7 +4,7 @@ #![warn(clippy::all)] use egui::{Slider, Window}; -use egui_glium::{storage::FileStorage, RunMode}; +use egui_glium::storage::FileStorage; /// We derive Deserialize/Serialize so we can persist app state on shutdown. #[derive(Default, serde::Deserialize, serde::Serialize)] @@ -39,7 +39,7 @@ fn main() { let title = "My Egui Window"; 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); + egui_glium::run(title, storage, app); } fn my_save_function() {