[app] Refactor egui::app::App interface to be more data oriented
This commit is contained in:
parent
e56924dc4f
commit
251cde60f0
7 changed files with 124 additions and 111 deletions
|
@ -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
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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")]
|
||||||
|
|
|
@ -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 mut ui = ctx.begin_frame(raw_input.take());
|
let backend_info = egui::app::BackendInfo {
|
||||||
app.ui(&mut ui, &mut runner);
|
web_info: None,
|
||||||
let (output, paint_jobs) = ctx.end_frame();
|
cpu_usage: previous_frame_time,
|
||||||
|
seconds_since_midnight: Some(seconds_since_midnight()),
|
||||||
let frame_time = (Instant::now() - egui_start).as_secs_f64() as f32;
|
|
||||||
runner.previous_frame_time = Some(frame_time);
|
|
||||||
|
|
||||||
runner
|
|
||||||
.painter
|
|
||||||
.paint_jobs(&display, paint_jobs, &ctx.texture());
|
|
||||||
|
|
||||||
*control_flow = if runner.quit {
|
|
||||||
glutin::event_loop::ControlFlow::Exit
|
|
||||||
} else if output.needs_repaint {
|
|
||||||
display.gl_window().window().request_redraw();
|
|
||||||
glutin::event_loop::ControlFlow::Poll
|
|
||||||
} else {
|
|
||||||
glutin::event_loop::ControlFlow::Wait
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handle_output(output, &display, clipboard.as_mut());
|
let mut ui = ctx.begin_frame(raw_input.take());
|
||||||
|
let app_output = app.ui(&mut ui, &backend_info, Some(&mut painter));
|
||||||
|
let (egui_output, paint_jobs) = ctx.end_frame();
|
||||||
|
|
||||||
|
let frame_time = (Instant::now() - egui_start).as_secs_f64() as f32;
|
||||||
|
previous_frame_time = Some(frame_time);
|
||||||
|
painter.paint_jobs(&display, paint_jobs, &ctx.texture());
|
||||||
|
|
||||||
|
{
|
||||||
|
let egui::app::AppOutput { quit } = app_output;
|
||||||
|
|
||||||
|
*control_flow = if quit {
|
||||||
|
glutin::event_loop::ControlFlow::Exit
|
||||||
|
} else if egui_output.needs_repaint {
|
||||||
|
display.gl_window().window().request_redraw();
|
||||||
|
glutin::event_loop::ControlFlow::Poll
|
||||||
|
} else {
|
||||||
|
glutin::event_loop::ControlFlow::Wait
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_output(egui_output, &display, clipboard.as_mut());
|
||||||
};
|
};
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
|
@ -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…
Reference in a new issue