Browse Source

[app] Refactor egui::app::App interface to be more data oriented

pull/30/head
Emil Ernerfeldt 4 years ago
parent
commit
251cde60f0
  1. 4
      CHANGELOG.md
  2. 2
      demo_web/src/lib.rs
  3. 33
      egui/src/app.rs
  4. 59
      egui/src/demos/app.rs
  5. 79
      egui_glium/src/backend.rs
  6. 45
      egui_web/src/backend.rs
  7. 9
      example_glium/src/main.rs

4
CHANGELOG.md

@ -2,6 +2,10 @@
## Unreleased ## Unreleased
* `ui.horizontal(...)` etc returns `Response`
* Add ability to override text color with `visuals.override_text_color`
* Refactored the interface for `egui::app::App`
## 0.2.0 - 2020-10-10 ## 0.2.0 - 2020-10-10
* Color picker * Color picker

2
demo_web/src/lib.rs

@ -9,6 +9,6 @@ pub fn start(canvas_id: &str) -> Result<(), wasm_bindgen::JsValue> {
let backend = egui_web::WebBackend::new(canvas_id)?; let backend = egui_web::WebBackend::new(canvas_id)?;
let app = Box::new(egui::DemoApp::default()); let app = Box::new(egui::DemoApp::default());
let runner = egui_web::AppRunner::new(backend, app)?; let runner = egui_web::AppRunner::new(backend, app)?;
egui_web::run(runner)?; egui_web::start(runner)?;
Ok(()) Ok(())
} }

33
egui/src/app.rs

@ -13,7 +13,12 @@ use crate::Ui;
/// and deployed as a web site using the [`egui_web`](https://crates.io/crates/egui_web) crate. /// and deployed as a web site using the [`egui_web`](https://crates.io/crates/egui_web) crate.
pub trait App { 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.
fn ui(&mut self, ui: &mut Ui, backend: &mut dyn Backend); fn ui(
&mut self,
ui: &mut Ui,
info: &BackendInfo,
tex_allocator: Option<&mut dyn TextureAllocator>,
) -> AppOutput;
/// Called once on shutdown. Allows you to save state. /// Called once on shutdown. Allows you to save state.
fn on_exit(&mut self, _storage: &mut dyn Storage) {} fn on_exit(&mut self, _storage: &mut dyn Storage) {}
@ -24,25 +29,29 @@ pub struct WebInfo {
pub web_location_hash: String, pub web_location_hash: String,
} }
pub trait Backend { /// Information about the backend passed to the use app each frame.
pub struct BackendInfo {
/// If the app is running in a Web context, this returns information about the environment. /// If the app is running in a Web context, this returns information about the environment.
fn web_info(&self) -> Option<WebInfo> { pub web_info: Option<WebInfo>,
None
}
/// Seconds of cpu usage (in seconds) of UI code on the previous frame. /// Seconds of cpu usage (in seconds) of UI code on the previous frame.
/// Zero if this is the first frame. /// `None` if this is the first frame.
fn cpu_usage(&self) -> Option<f32>; pub cpu_usage: Option<f32>,
/// Local time. Used for the clock in the demo app. /// Local time. Used for the clock in the demo app.
fn seconds_since_midnight(&self) -> Option<f64> { /// Set to `None` if you don't know.
None pub seconds_since_midnight: Option<f64>,
} }
/// Signal the backend that we'd like to exit the app now. /// Action that can be taken by the user app.
#[derive(Clone, Copy, Debug, Default)]
pub struct AppOutput {
/// Set to `true` to stop the app.
/// This does nothing for web apps. /// This does nothing for web apps.
fn quit(&mut self) {} pub quit: bool,
}
pub trait TextureAllocator {
/// Allocate a user texture (EXPERIMENTAL!) /// Allocate a user texture (EXPERIMENTAL!)
fn new_texture_srgba_premultiplied( fn new_texture_srgba_premultiplied(
&mut self, &mut self,

59
egui/src/demos/app.rs

@ -314,11 +314,13 @@ impl DemoApp {
} }
// TODO: give cpu_usage and web_info via `struct BackendInfo` // TODO: give cpu_usage and web_info via `struct BackendInfo`
fn backend_ui(&mut self, ui: &mut Ui, backend: &mut dyn app::Backend) { fn backend_ui(&mut self, ui: &mut Ui, info: &app::BackendInfo) -> app::AppOutput {
self.frame_history self.frame_history
.on_new_frame(ui.input().time, backend.cpu_usage()); .on_new_frame(ui.input().time, info.cpu_usage);
let is_web = backend.web_info().is_some(); let is_web = info.web_info.is_some();
let mut options = app::AppOutput::default();
if is_web { if is_web {
ui.label("Egui is an immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL."); ui.label("Egui is an immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL.");
@ -332,10 +334,7 @@ impl DemoApp {
}); });
} else { } else {
ui.heading("Egui"); ui.heading("Egui");
if ui.button("Quit").clicked { options.quit |= ui.button("Quit").clicked;
backend.quit();
return;
}
} }
ui.separator(); ui.separator();
@ -359,6 +358,8 @@ impl DemoApp {
&mut self.show_color_test, &mut self.show_color_test,
"Show color blend test (debug backend painter)", "Show color blend test (debug backend painter)",
); );
options
} }
fn run_mode_ui(&mut self, ui: &mut Ui) { fn run_mode_ui(&mut self, ui: &mut Ui) {
@ -374,12 +375,19 @@ impl DemoApp {
} }
impl app::App for DemoApp { impl app::App for DemoApp {
fn ui(&mut self, ui: &mut Ui, backend: &mut dyn app::Backend) { fn ui(
&mut self,
ui: &mut Ui,
info: &app::BackendInfo,
tex_allocator: Option<&mut dyn app::TextureAllocator>,
) -> app::AppOutput {
let mut output = app::AppOutput::default();
Window::new("Backend") Window::new("Backend")
.min_width(360.0) .min_width(360.0)
.scroll(false) .scroll(false)
.show(ui.ctx(), |ui| { .show(ui.ctx(), |ui| {
self.backend_ui(ui, backend); output = self.backend_ui(ui, info);
}); });
let Self { let Self {
@ -388,28 +396,31 @@ impl app::App for DemoApp {
.. ..
} = self; } = self;
if *show_color_test { // TODO: enable color test even without `tex_allocator`
let mut tex_loader = |size: (usize, usize), pixels: &[Srgba]| { if let Some(tex_allocator) = tex_allocator {
backend.new_texture_srgba_premultiplied(size, pixels) if *show_color_test {
}; let mut tex_loader = |size: (usize, usize), pixels: &[Srgba]| {
Window::new("Color Test") tex_allocator.new_texture_srgba_premultiplied(size, pixels)
.default_size(vec2(1024.0, 1024.0)) };
.scroll(true) Window::new("Color Test")
.open(show_color_test) .default_size(vec2(1024.0, 1024.0))
.show(ui.ctx(), |ui| { .scroll(true)
color_test.ui(ui, &mut tex_loader); .open(show_color_test)
}); .show(ui.ctx(), |ui| {
color_test.ui(ui, &mut tex_loader);
});
}
} }
let web_info = backend.web_info(); let web_location_hash = info
let web_location_hash = web_info .web_info
.as_ref() .as_ref()
.map(|info| info.web_location_hash.clone()) .map(|info| info.web_location_hash.clone())
.unwrap_or_default(); .unwrap_or_default();
let environment = DemoEnvironment { let environment = DemoEnvironment {
web_location_hash, web_location_hash,
seconds_since_midnight: backend.seconds_since_midnight(), seconds_since_midnight: info.seconds_since_midnight,
}; };
self.ui(ui, &environment); self.ui(ui, &environment);
@ -418,6 +429,8 @@ impl app::App for DemoApp {
// Tell the backend to repaint as soon as possible // Tell the backend to repaint as soon as possible
ui.ctx().request_repaint(); ui.ctx().request_repaint();
} }
output
} }
#[cfg(feature = "serde_json")] #[cfg(feature = "serde_json")]

79
egui_glium/src/backend.rs

@ -6,48 +6,20 @@ use crate::{
}; };
pub use egui::{ pub use egui::{
app::{App, Backend, Storage}, app::{self, App, Storage},
Srgba, Srgba,
}; };
const EGUI_MEMORY_KEY: &str = "egui"; const EGUI_MEMORY_KEY: &str = "egui";
const WINDOW_KEY: &str = "window"; const WINDOW_KEY: &str = "window";
pub struct GliumBackend { impl egui::app::TextureAllocator for Painter {
previous_frame_time: Option<f32>,
quit: bool,
painter: Painter,
}
impl GliumBackend {
pub fn new(painter: Painter) -> Self {
Self {
previous_frame_time: None,
quit: false,
painter,
}
}
}
impl Backend for GliumBackend {
fn cpu_usage(&self) -> Option<f32> {
self.previous_frame_time
}
fn seconds_since_midnight(&self) -> Option<f64> {
Some(seconds_since_midnight())
}
fn quit(&mut self) {
self.quit = true;
}
fn new_texture_srgba_premultiplied( fn new_texture_srgba_premultiplied(
&mut self, &mut self,
size: (usize, usize), size: (usize, usize),
pixels: &[Srgba], pixels: &[Srgba],
) -> egui::TextureId { ) -> egui::TextureId {
self.painter.new_user_texture(size, pixels) self.new_user_texture(size, pixels)
} }
} }
@ -81,9 +53,9 @@ pub fn run(title: &str, mut storage: FileStorage, mut app: impl App + 'static) -
let mut raw_input = make_raw_input(&display); let mut raw_input = make_raw_input(&display);
// used to keep track of time for animations
let start_time = Instant::now(); let start_time = Instant::now();
let mut runner = GliumBackend::new(Painter::new(&display)); let mut previous_frame_time = None;
let mut painter = Painter::new(&display);
let mut clipboard = init_clipboard(); let mut clipboard = init_clipboard();
event_loop.run(move |event, _, control_flow| { event_loop.run(move |event, _, control_flow| {
@ -91,27 +63,34 @@ pub fn run(title: &str, mut storage: FileStorage, mut app: impl App + 'static) -
let egui_start = Instant::now(); let egui_start = Instant::now();
raw_input.time = start_time.elapsed().as_nanos() as f64 * 1e-9; raw_input.time = start_time.elapsed().as_nanos() as f64 * 1e-9;
let backend_info = egui::app::BackendInfo {
web_info: None,
cpu_usage: previous_frame_time,
seconds_since_midnight: Some(seconds_since_midnight()),
};
let mut ui = ctx.begin_frame(raw_input.take()); let mut ui = ctx.begin_frame(raw_input.take());
app.ui(&mut ui, &mut runner); let app_output = app.ui(&mut ui, &backend_info, Some(&mut painter));
let (output, paint_jobs) = ctx.end_frame(); let (egui_output, paint_jobs) = ctx.end_frame();
let frame_time = (Instant::now() - egui_start).as_secs_f64() as f32; let frame_time = (Instant::now() - egui_start).as_secs_f64() as f32;
runner.previous_frame_time = Some(frame_time); previous_frame_time = Some(frame_time);
painter.paint_jobs(&display, paint_jobs, &ctx.texture());
runner
.painter {
.paint_jobs(&display, paint_jobs, &ctx.texture()); let egui::app::AppOutput { quit } = app_output;
*control_flow = if runner.quit { *control_flow = if quit {
glutin::event_loop::ControlFlow::Exit glutin::event_loop::ControlFlow::Exit
} else if output.needs_repaint { } else if egui_output.needs_repaint {
display.gl_window().window().request_redraw(); display.gl_window().window().request_redraw();
glutin::event_loop::ControlFlow::Poll glutin::event_loop::ControlFlow::Poll
} else { } else {
glutin::event_loop::ControlFlow::Wait glutin::event_loop::ControlFlow::Wait
}; };
}
handle_output(output, &display, clipboard.as_mut()); handle_output(egui_output, &display, clipboard.as_mut());
}; };
match event { match event {

45
egui_web/src/backend.rs

@ -1,7 +1,7 @@
use crate::*; use crate::*;
pub use egui::{ pub use egui::{
app::{App, Backend, WebInfo}, app::{App, WebInfo},
Srgba, Srgba,
}; };
@ -80,27 +80,13 @@ impl WebBackend {
} }
} }
impl Backend for WebBackend { impl egui::app::TextureAllocator for webgl::Painter {
fn web_info(&self) -> Option<WebInfo> {
Some(WebInfo {
web_location_hash: location_hash().unwrap_or_default(),
})
}
fn cpu_usage(&self) -> Option<f32> {
self.previous_frame_time
}
fn seconds_since_midnight(&self) -> Option<f64> {
Some(seconds_since_midnight())
}
fn new_texture_srgba_premultiplied( fn new_texture_srgba_premultiplied(
&mut self, &mut self,
size: (usize, usize), size: (usize, usize),
pixels: &[Srgba], pixels: &[Srgba],
) -> egui::TextureId { ) -> egui::TextureId {
self.painter.new_user_texture(size, pixels) self.new_user_texture(size, pixels)
} }
} }
@ -160,11 +146,26 @@ impl AppRunner {
let raw_input = self.web_input.new_frame(); let raw_input = self.web_input.new_frame();
let backend_info = egui::app::BackendInfo {
web_info: Some(WebInfo {
web_location_hash: location_hash().unwrap_or_default(),
}),
cpu_usage: self.web_backend.previous_frame_time,
seconds_since_midnight: Some(seconds_since_midnight()),
};
let mut ui = self.web_backend.begin_frame(raw_input); let mut ui = self.web_backend.begin_frame(raw_input);
self.app.ui(&mut ui, &mut self.web_backend); let app_output = self
let (output, paint_jobs) = self.web_backend.end_frame()?; .app
handle_output(&output); .ui(&mut ui, &backend_info, Some(&mut self.web_backend.painter));
Ok((output, paint_jobs)) let (egui_output, paint_jobs) = self.web_backend.end_frame()?;
handle_output(&egui_output);
{
let egui::app::AppOutput { quit: _ } = app_output;
}
Ok((egui_output, paint_jobs))
} }
pub fn paint(&mut self, paint_jobs: egui::PaintJobs) -> Result<(), JsValue> { pub fn paint(&mut self, paint_jobs: egui::PaintJobs) -> Result<(), JsValue> {
@ -174,7 +175,7 @@ impl AppRunner {
/// Install event listeners to register different input events /// Install event listeners to register different input events
/// and starts running the given `AppRunner`. /// and starts running the given `AppRunner`.
pub fn run(app_runner: AppRunner) -> Result<AppRunnerRef, JsValue> { pub fn start(app_runner: AppRunner) -> Result<AppRunnerRef, JsValue> {
let runner_ref = AppRunnerRef(Arc::new(Mutex::new(app_runner))); let runner_ref = AppRunnerRef(Arc::new(Mutex::new(app_runner)));
install_canvas_events(&runner_ref)?; install_canvas_events(&runner_ref)?;
install_document_events(&runner_ref)?; install_document_events(&runner_ref)?;

9
example_glium/src/main.rs

@ -16,7 +16,12 @@ struct MyApp {
impl egui::app::App for MyApp { impl egui::app::App for MyApp {
/// This function will be called whenever the Ui needs to be shown, /// This function will be called whenever the Ui needs to be shown,
/// which may be many times per second. /// which may be many times per second.
fn ui(&mut self, ui: &mut egui::Ui, _: &mut dyn egui::app::Backend) { fn ui(
&mut self,
ui: &mut egui::Ui,
_info: &egui::app::BackendInfo,
_tex_allocator: Option<&mut dyn egui::app::TextureAllocator>,
) -> egui::app::AppOutput {
let MyApp { my_string, value } = self; let MyApp { my_string, value } = self;
// Example used in `README.md`. // Example used in `README.md`.
@ -28,6 +33,8 @@ impl egui::app::App for MyApp {
ui.text_edit(my_string); ui.text_edit(my_string);
ui.add(Slider::f32(value, 0.0..=1.0).text("float")); ui.add(Slider::f32(value, 0.0..=1.0).text("float"));
}); });
Default::default()
} }
fn on_exit(&mut self, storage: &mut dyn egui::app::Storage) { fn on_exit(&mut self, storage: &mut dyn egui::app::Storage) {

Loading…
Cancel
Save