[app] unify web and glium demo app

This commit is contained in:
Emil Ernerfeldt 2020-07-23 18:54:16 +02:00
parent b79c76b9ce
commit 554e6e7120
15 changed files with 445 additions and 442 deletions

8
Cargo.lock generated
View file

@ -427,12 +427,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "egui"
version = "0.1.2"
source = "git+https://github.com/emilk/emigui#3552f7f82861fb10ae646c61f3c283256e79940e"
source = "git+https://github.com/emilk/emigui#e3d1d6c99ce37090d496b4f210aaef700235dd9b"
dependencies = [
"ahash 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rusttype 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.56 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -444,12 +445,13 @@ dependencies = [
"parking_lot 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rusttype 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.56 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "egui_glium"
version = "0.1.0"
source = "git+https://github.com/emilk/emigui#3552f7f82861fb10ae646c61f3c283256e79940e"
source = "git+https://github.com/emilk/emigui#e3d1d6c99ce37090d496b4f210aaef700235dd9b"
dependencies = [
"chrono 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)",
"clipboard 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -476,7 +478,7 @@ dependencies = [
[[package]]
name = "egui_web"
version = "0.1.0"
source = "git+https://github.com/emilk/emigui#3552f7f82861fb10ae646c61f3c283256e79940e"
source = "git+https://github.com/emilk/emigui#e3d1d6c99ce37090d496b4f210aaef700235dd9b"
dependencies = [
"egui 0.1.2 (git+https://github.com/emilk/emigui)",
"js-sys 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)",

View file

@ -1,70 +1,11 @@
#![deny(warnings)]
#![warn(clippy::all)]
use egui_glium::{persistence::Persistence, RunMode, Runner};
const APP_KEY: &str = "app";
#[derive(Default, serde::Deserialize, serde::Serialize)]
struct MyApp {
egui_demo_app: egui::DemoApp,
frames_painted: u64,
}
impl egui_glium::App for MyApp {
fn ui(&mut self, ui: &mut egui::Ui, runner: &mut Runner) {
self.egui_demo_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 inside of Glium").text_style(TextStyle::Heading));
if ui.add(Button::new("Quit")).clicked {
runner.quit();
return;
}
ui.add(
label!(
"CPU usage: {:.2} ms / frame (excludes painting)",
1e3 * runner.cpu_time()
)
.text_style(TextStyle::Monospace),
);
ui.separator();
ui.horizontal(|ui| {
let mut run_mode = runner.run_mode();
ui.label("Run mode:");
ui.radio_value("Continuous", &mut run_mode, RunMode::Continuous)
.tooltip_text("Repaint everything each frame");
ui.radio_value("Reactive", &mut run_mode, RunMode::Reactive)
.tooltip_text("Repaint when there are animations or input (e.g. mouse movement)");
runner.set_run_mode(run_mode);
});
if runner.run_mode() == RunMode::Continuous {
ui.add(
label!("Repainting the UI each frame. FPS: {:.1}", runner.fps())
.text_style(TextStyle::Monospace),
);
} else {
ui.label("Only running UI code when there are animations or input");
}
self.frames_painted += 1;
ui.label(format!("Total frames painted: {}", self.frames_painted));
}
fn on_exit(&mut self, persistence: &mut Persistence) {
persistence.set_value(APP_KEY, self);
}
}
use egui_glium::{storage::FileStorage, RunMode};
fn main() {
let title = "Egui glium demo";
let persistence = Persistence::from_path(".egui_demo_glium.json".into());
let app: MyApp = persistence.get_value(APP_KEY).unwrap_or_default();
egui_glium::run(title, RunMode::Reactive, persistence, app);
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);
}

View file

@ -1,86 +1,14 @@
#![deny(warnings)]
#![warn(clippy::all)]
use egui::{label, TextStyle};
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::Backend::new(canvas_id, egui_web::RunMode::Reactive)?;
let app = Box::new(MyApp::default());
let backend = egui_web::WebBackend::new(canvas_id, egui_web::RunMode::Reactive)?;
let app = Box::new(egui::DemoApp::default());
let runner = egui_web::AppRunner::new(backend, app)?;
egui_web::run(runner)?;
Ok(())
}
// ----------------------------------------------------------------------------
#[derive(Default)]
pub struct MyApp {
demo_app: egui::demos::DemoApp,
frames_painted: u64,
}
impl MyApp {
fn window_ui(&mut self, ui: &mut egui::Ui, backend: &mut egui_web::Backend) {
ui.label("Egui is an immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL.");
ui.label(
"Everything you see is rendered as textured triangles. There is no DOM. There are no HTML elements."
);
ui.label("This is not JavaScript. This is Rust, running at 60 FPS. This is the web page, reinvented with game tech.");
ui.label("This is also work in progress, and not ready for production... yet :)");
ui.horizontal(|ui| {
ui.label("Project home page:");
ui.hyperlink("https://github.com/emilk/emigui/");
});
ui.separator();
ui.add(
label!(
"CPU usage: {:.2} ms / frame (excludes painting)",
1e3 * backend.cpu_time()
)
.text_style(TextStyle::Monospace),
);
ui.separator();
ui.horizontal(|ui| {
let mut run_mode = backend.run_mode();
ui.label("Run mode:");
ui.radio_value("Continuous", &mut run_mode, egui_web::RunMode::Continuous)
.tooltip_text("Repaint everything each frame");
ui.radio_value("Reactive", &mut run_mode, egui_web::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() == egui_web::RunMode::Continuous {
ui.add(
label!("Repainting the UI each frame. FPS: {:.1}", backend.fps())
.text_style(TextStyle::Monospace),
);
} else {
ui.label("Only running UI code when there are animations or input");
}
self.frames_painted += 1;
ui.label(format!("Total frames painted: {}", self.frames_painted));
}
}
impl egui_web::App for MyApp {
fn ui(&mut self, ui: &mut egui::Ui, backend: &mut egui_web::Backend, info: &egui_web::WebInfo) {
egui::Window::new("Egui")
.default_width(500.0)
.show(ui.ctx(), |ui| {
self.window_ui(ui, backend);
});
self.demo_app.ui(ui, &info.web_location_hash);
}
}

View file

@ -208,27 +208,27 @@ function makeMutClosure(arg0, arg1, dtor, f) {
return real;
}
function __wbg_adapter_26(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h22fd33d9f501a695(arg0, arg1, addHeapObject(arg2));
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h426caa8a7645dc38(arg0, arg1, addHeapObject(arg2));
}
function __wbg_adapter_29(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h22fd33d9f501a695(arg0, arg1, addHeapObject(arg2));
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h426caa8a7645dc38(arg0, arg1, addHeapObject(arg2));
}
function __wbg_adapter_32(arg0, arg1) {
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h5402719cc6dde927(arg0, arg1);
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb0f876788be788af(arg0, arg1);
}
function __wbg_adapter_35(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h22fd33d9f501a695(arg0, arg1, addHeapObject(arg2));
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h426caa8a7645dc38(arg0, arg1, addHeapObject(arg2));
}
function __wbg_adapter_38(arg0, arg1) {
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h84da5f062b972f09(arg0, arg1);
function __wbg_adapter_38(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h426caa8a7645dc38(arg0, arg1, addHeapObject(arg2));
}
function __wbg_adapter_41(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h22fd33d9f501a695(arg0, arg1, addHeapObject(arg2));
function __wbg_adapter_41(arg0, arg1) {
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h400d42c2242ce5bd(arg0, arg1);
}
/**
@ -304,6 +304,10 @@ async function init(input) {
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
takeObject(arg0);
};
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
var ret = getStringFromWasm0(arg0, arg1);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_cb_forget = function(arg0) {
takeObject(arg0);
};
@ -316,10 +320,6 @@ async function init(input) {
var ret = false;
return ret;
};
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
var ret = getStringFromWasm0(arg0, arg1);
return addHeapObject(ret);
};
imports.wbg.__wbg_instanceof_Window_0e8decd0a6179699 = function(arg0) {
var ret = getObject(arg0) instanceof Window;
return ret;
@ -717,28 +717,28 @@ async function init(input) {
var ret = wasm.memory;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper380 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 68, __wbg_adapter_26);
imports.wbg.__wbindgen_closure_wrapper364 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 76, __wbg_adapter_35);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper366 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 76, __wbg_adapter_38);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper363 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 76, __wbg_adapter_41);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper368 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 76, __wbg_adapter_26);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper370 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 68, __wbg_adapter_32);
var ret = makeMutClosure(arg0, arg1, 76, __wbg_adapter_29);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper378 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 68, __wbg_adapter_41);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper376 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 68, __wbg_adapter_38);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper374 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 68, __wbg_adapter_35);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper371 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 68, __wbg_adapter_29);
imports.wbg.__wbindgen_closure_wrapper373 = function(arg0, arg1, arg2) {
var ret = makeMutClosure(arg0, arg1, 76, __wbg_adapter_32);
return addHeapObject(ret);
};

Binary file not shown.

View file

@ -19,9 +19,10 @@ ahash = "0.4"
parking_lot = "0.11"
rusttype = "0.9"
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true }
[features]
with_serde = ["serde"]
with_serde = ["serde", "serde_json"]
[dev-dependencies]
criterion = { version = "0.3", default-features = false }

79
egui/src/app.rs Normal file
View file

@ -0,0 +1,79 @@
//! Traits and helper for writing Egui apps.
//!
//! Egui can be used as a library, but you can also use it as a framework to write apps in.
//! This module defined the `App` trait that can be implemented and used with the `egui_web` and `egui_glium` crates.
use crate::Ui;
/// Implement this trait to write apps that can be compiled both natively using the `egui_glium` crate,
/// and deployed as a web site using the `egui_web` crate.
pub trait App {
/// 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);
/// Called once on shutdown. Allows you to save state.
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 {
/// Rapint 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<WebInfo> {
None
}
/// excludes painting
fn cpu_time(&self) -> f32;
/// Smoothed frames per second
fn fps(&self) -> f32;
/// Signal the backend that we'd like to exit the app now.
/// This does nothing for web apps.s
fn quit(&mut self) {}
}
/// A place where you can store custom data in a way that persists when you restart the app.
///
/// On the web this is backed by [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
/// On desktop this is backed by the file system.
pub trait Storage {
fn get_string(&self, key: &str) -> Option<&str>;
fn set_string(&mut self, key: &str, value: String);
}
#[cfg(feature = "with_serde")]
pub fn get_value<T: serde::de::DeserializeOwned>(storage: &dyn Storage, key: &str) -> Option<T> {
storage
.get_string(key)
.and_then(|value| serde_json::from_str(value).ok())
}
#[cfg(feature = "with_serde")]
pub fn set_value<T: serde::Serialize>(storage: &mut dyn Storage, key: &str, value: &T) {
storage.set_string(key, serde_json::to_string(value).unwrap());
}
/// storage key used for app
pub const APP_KEY: &str = "app";

View file

@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::{color::*, containers::*, demos::FractalClock, paint::*, widgets::*, *};
use crate::{app, color::*, containers::*, demos::FractalClock, paint::*, widgets::*, *};
// ----------------------------------------------------------------------------
@ -12,6 +12,7 @@ pub struct DemoApp {
open_windows: OpenWindows,
demo_window: DemoWindow,
fractal_clock: FractalClock,
num_frames_painted: u64,
}
impl DemoApp {
@ -68,6 +69,84 @@ impl DemoApp {
fractal_clock.window(ctx, &mut open_windows.fractal_clock);
}
fn backend_ui(&mut self, ui: &mut Ui, backend: &mut dyn app::Backend) {
let is_web = backend.web_info().is_some();
if is_web {
ui.label("Egui is an immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL.");
ui.label(
"Everything you see is rendered as textured triangles. There is no DOM. There are no HTML elements."
);
ui.label("This is not JavaScript. This is Rust, running at 60 FPS. This is the web page, reinvented with game tech.");
ui.label("This is also work in progress, and not ready for production... yet :)");
ui.horizontal(|ui| {
ui.label("Project home page:");
ui.hyperlink("https://github.com/emilk/emigui/");
});
} else {
ui.add(label!("Egui").text_style(TextStyle::Heading));
if ui.add(Button::new("Quit")).clicked {
backend.quit();
return;
}
}
ui.separator();
ui.add(
label!(
"CPU usage: {:.2} ms / frame (excludes painting)",
1e3 * backend.cpu_time()
)
.text_style(TextStyle::Monospace),
);
ui.separator();
ui.horizontal(|ui| {
let mut run_mode = backend.run_mode();
ui.label("Run mode:");
ui.radio_value("Continuous", &mut run_mode, app::RunMode::Continuous)
.tooltip_text("Repaint everything each frame");
ui.radio_value("Reactive", &mut run_mode, app::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 {
ui.add(
label!("Repainting the UI each frame. FPS: {:.1}", backend.fps())
.text_style(TextStyle::Monospace),
);
} else {
ui.label("Only running UI code when there are animations or input");
}
self.num_frames_painted += 1;
ui.label(format!("Total frames painted: {}", self.num_frames_painted));
}
}
impl app::App for DemoApp {
fn ui(&mut self, ui: &mut Ui, backend: &mut dyn app::Backend) {
Window::new("Backend")
.default_width(500.0)
.show(ui.ctx(), |ui| {
self.backend_ui(ui, backend);
});
let web_info = backend.web_info();
let web_location_hash = web_info
.as_ref()
.map(|info| info.web_location_hash.as_str())
.unwrap_or_default();
self.ui(ui, web_location_hash);
}
fn on_exit(&mut self, storage: &mut dyn app::Storage) {
app::set_value(storage, app::APP_KEY, self);
}
}
// ----------------------------------------------------------------------------

View file

@ -22,6 +22,7 @@
rust_2018_idioms,
)]
pub mod app;
pub mod containers;
mod context;
pub mod demos;

View file

@ -1,38 +1,22 @@
use std::time::Instant;
use crate::{
persistence::{Persistence, WindowSettings},
storage::{FileStorage, WindowSettings},
*,
};
pub use egui::app::{App, Backend, RunMode, Storage};
const EGUI_MEMORY_KEY: &str = "egui";
const WINDOW_KEY: &str = "window";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RunMode {
/// Uses `request_animation_frame` to repaint the UI on each display Hz.
/// This is good for games and stuff where you want to run logic at e.g. 60 FPS.
Continuous,
/// Only repaint when there are animations or input (mouse movement, keyboard input etc).
Reactive,
}
pub trait App {
/// Called onced per frame for you to draw the UI.
fn ui(&mut self, ui: &mut egui::Ui, runner: &mut Runner);
/// Called once on shutdown. Allows you to save state.
fn on_exit(&mut self, persistence: &mut Persistence);
}
pub struct Runner {
pub struct GliumBackend {
frame_times: egui::MovementTracker<f32>,
quit: bool,
run_mode: RunMode,
}
impl Runner {
impl GliumBackend {
pub fn new(run_mode: RunMode) -> Self {
Self {
frame_times: egui::MovementTracker::new(1000, 1.0),
@ -40,33 +24,35 @@ impl Runner {
run_mode,
}
}
}
pub fn run_mode(&self) -> RunMode {
impl Backend for GliumBackend {
fn run_mode(&self) -> RunMode {
self.run_mode
}
pub fn set_run_mode(&mut self, run_mode: RunMode) {
fn set_run_mode(&mut self, run_mode: RunMode) {
self.run_mode = run_mode;
}
pub fn quit(&mut self) {
self.quit = true;
}
pub fn cpu_time(&self) -> f32 {
fn cpu_time(&self) -> f32 {
self.frame_times.average().unwrap_or_default()
}
pub fn fps(&self) -> f32 {
fn fps(&self) -> f32 {
1.0 / self.frame_times.mean_time_interval().unwrap_or_default()
}
fn quit(&mut self) {
self.quit = true;
}
}
/// Run an egui app
pub fn run(
title: &str,
run_mode: RunMode,
mut persistence: Persistence,
mut storage: FileStorage,
mut app: impl App + 'static,
) -> ! {
let event_loop = glutin::event_loop::EventLoop::new();
@ -76,7 +62,7 @@ pub fn run(
.with_title(title)
.with_transparent(false);
let window_settings: Option<WindowSettings> = persistence.get_value(WINDOW_KEY);
let window_settings: Option<WindowSettings> = egui::app::get_value(&storage, WINDOW_KEY);
if let Some(window_settings) = &window_settings {
window = window_settings.initialize_size(window);
}
@ -93,14 +79,14 @@ pub fn run(
}
let mut ctx = egui::Context::new();
*ctx.memory() = persistence.get_value(EGUI_MEMORY_KEY).unwrap_or_default();
*ctx.memory() = egui::app::get_value(&storage, EGUI_MEMORY_KEY).unwrap_or_default();
let mut painter = 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 runner = Runner::new(run_mode);
let mut runner = GliumBackend::new(run_mode);
let mut clipboard = init_clipboard();
event_loop.run(move |event, _, control_flow| {
@ -134,10 +120,14 @@ pub fn run(
display.gl_window().window().request_redraw(); // TODO: maybe only on some events?
}
glutin::event::Event::LoopDestroyed => {
persistence.set_value(WINDOW_KEY, &WindowSettings::from_display(&display));
persistence.set_value(EGUI_MEMORY_KEY, &*ctx.memory());
app.on_exit(&mut persistence);
persistence.save();
egui::app::set_value(
&mut storage,
WINDOW_KEY,
&WindowSettings::from_display(&display),
);
egui::app::set_value(&mut storage, EGUI_MEMORY_KEY, &*ctx.memory());
app.on_exit(&mut storage);
storage.save();
}
_ => (),
}

View file

@ -3,12 +3,12 @@
#![allow(clippy::single_match)]
#![allow(deprecated)] // TODO: remove
mod backend;
mod painter;
pub mod persistence;
mod runner;
pub mod storage;
pub use backend::*;
pub use painter::Painter;
pub use runner::*;
use {
clipboard::ClipboardProvider,

View file

@ -4,13 +4,13 @@ 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 {
pub struct FileStorage {
path: String,
kv: HashMap<String, String>,
dirty: bool,
}
impl Persistence {
impl FileStorage {
pub fn from_path(path: String) -> Self {
Self {
kv: read_json(&path).unwrap_or_default(),
@ -19,23 +19,6 @@ impl Persistence {
}
}
pub fn get_value<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
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<T: serde::Serialize>(&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();
@ -44,6 +27,19 @@ impl Persistence {
}
}
impl egui::app::Storage for FileStorage {
fn get_string(&self, key: &str) -> Option<&str> {
self.kv.get(key).map(String::as_str)
}
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 read_json<T>(memory_json_path: impl AsRef<std::path::Path>) -> Option<T>
@ -69,7 +65,7 @@ where
}
// ----------------------------------------------------------------------------
/// Alternative to `Persistence`
/// Alternative to `FileStorage`
pub fn read_memory(ctx: &egui::Context, memory_json_path: impl AsRef<std::path::Path>) {
let memory: Option<egui::Memory> = read_json(memory_json_path);
if let Some(memory) = memory {
@ -77,7 +73,7 @@ pub fn read_memory(ctx: &egui::Context, memory_json_path: impl AsRef<std::path::
}
}
/// Alternative to `Persistence`
/// Alternative to `FileStorage`
pub fn write_memory(
ctx: &egui::Context,
memory_json_path: impl AsRef<std::path::Path>,

184
egui_web/src/backend.rs Normal file
View file

@ -0,0 +1,184 @@
use crate::*;
pub use egui::app::{App, Backend, RunMode, WebInfo};
// ----------------------------------------------------------------------------
pub struct WebBackend {
ctx: Arc<egui::Context>,
painter: webgl::Painter,
frame_times: egui::MovementTracker<f32>,
frame_start: Option<f64>,
run_mode: RunMode,
last_save_time: Option<f64>,
}
impl WebBackend {
pub fn new(canvas_id: &str, run_mode: RunMode) -> Result<Self, JsValue> {
let ctx = egui::Context::new();
load_memory(&ctx);
Ok(Self {
ctx,
painter: webgl::Painter::new(canvas_id)?,
frame_times: egui::MovementTracker::new(1000, 1.0),
frame_start: None,
run_mode,
last_save_time: None,
})
}
/// id of the canvas html element containing the rendering
pub fn canvas_id(&self) -> &str {
self.painter.canvas_id()
}
/// Returns a master fullscreen UI, covering the entire screen.
pub fn begin_frame(&mut self, raw_input: egui::RawInput) -> egui::Ui {
self.frame_start = Some(now_sec());
self.ctx.begin_frame(raw_input)
}
pub fn end_frame(&mut self) -> Result<(egui::Output, egui::PaintJobs), JsValue> {
let frame_start = self
.frame_start
.take()
.expect("unmatched calls to begin_frame/end_frame");
let (output, paint_jobs) = self.ctx.end_frame();
self.auto_save();
let now = now_sec();
self.frame_times.add(now, (now - frame_start) as f32);
Ok((output, paint_jobs))
}
pub fn paint(&mut self, paint_jobs: egui::PaintJobs) -> Result<(), JsValue> {
let bg_color = egui::color::TRANSPARENT; // Use background css color.
self.painter.paint_jobs(
bg_color,
paint_jobs,
self.ctx.texture(),
self.ctx.pixels_per_point(),
)
}
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 {
self.painter.debug_info()
}
}
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<WebInfo> {
Some(WebInfo {
web_location_hash: location_hash().unwrap_or_default(),
})
}
/// excludes painting
fn cpu_time(&self) -> f32 {
self.frame_times.average().unwrap_or_default()
}
fn fps(&self) -> f32 {
1.0 / self.frame_times.mean_time_interval().unwrap_or_default()
}
}
// ----------------------------------------------------------------------------
// TODO: Just use RawInput?
/// Data gathered between frames.
/// Is translated to `egui::RawInput` at the start of each frame.
#[derive(Default)]
pub struct WebInput {
pub mouse_pos: Option<egui::Pos2>,
pub mouse_down: bool, // TODO: which button
pub is_touch: bool,
pub scroll_delta: egui::Vec2,
pub events: Vec<egui::Event>,
}
impl WebInput {
pub fn new_frame(&mut self) -> egui::RawInput {
egui::RawInput {
mouse_down: self.mouse_down,
mouse_pos: self.mouse_pos,
scroll_delta: std::mem::take(&mut self.scroll_delta),
screen_size: screen_size().unwrap(),
pixels_per_point: Some(pixels_per_point()),
time: now_sec(),
seconds_since_midnight: Some(seconds_since_midnight()),
events: std::mem::take(&mut self.events),
}
}
}
// ----------------------------------------------------------------------------
pub struct AppRunner {
pub web_backend: WebBackend,
pub web_input: WebInput,
pub app: Box<dyn App>,
pub needs_repaint: bool, // TODO: move
}
impl AppRunner {
pub fn new(web_backend: WebBackend, app: Box<dyn App>) -> Result<Self, JsValue> {
Ok(Self {
web_backend,
web_input: Default::default(),
app,
needs_repaint: true, // TODO: move
})
}
pub fn canvas_id(&self) -> &str {
self.web_backend.canvas_id()
}
pub fn logic(&mut self) -> Result<(egui::Output, egui::PaintJobs), JsValue> {
resize_to_screen_size(self.web_backend.canvas_id());
let raw_input = self.web_input.new_frame();
let mut ui = self.web_backend.begin_frame(raw_input);
self.app.ui(&mut ui, &mut self.web_backend);
let (output, paint_jobs) = self.web_backend.end_frame()?;
handle_output(&output);
Ok((output, paint_jobs))
}
pub fn paint(&mut self, paint_jobs: egui::PaintJobs) -> Result<(), JsValue> {
self.web_backend.paint(paint_jobs)
}
}
/// Install event listeners to register different input events
/// and starts running the given `AppRunner`.
pub fn run(app_runner: AppRunner) -> Result<AppRunnerRef, JsValue> {
let runner_ref = AppRunnerRef(Arc::new(Mutex::new(app_runner)));
install_canvas_events(&runner_ref)?;
install_document_events(&runner_ref)?;
paint_and_schedule(runner_ref.clone())?;
Ok(runner_ref)
}

View file

@ -1,211 +1,15 @@
#![deny(warnings)]
#![warn(clippy::all)]
pub mod backend;
pub mod webgl;
pub use backend::*;
use parking_lot::Mutex;
use std::sync::Arc;
use wasm_bindgen::prelude::*;
// ----------------------------------------------------------------------------
pub struct WebInfo {
/// e.g. "#fragment" part of "www.example.com/index.html#fragment"
pub web_location_hash: String,
}
/// Implement this and use `egui_web::AppRunner` to run your app.
pub trait App {
fn ui(&mut self, ui: &mut egui::Ui, backend: &mut Backend, info: &WebInfo);
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RunMode {
/// Uses `request_animation_frame` to repaint the UI on each display Hz.
/// This is good for games and stuff where you want to run logic at e.g. 60 FPS.
Continuous,
/// Only repaint when there are animations or input (mouse movement, keyboard input etc).
Reactive,
}
// ----------------------------------------------------------------------------
pub struct Backend {
ctx: Arc<egui::Context>,
painter: webgl::Painter,
frame_times: egui::MovementTracker<f32>,
frame_start: Option<f64>,
run_mode: RunMode,
last_save_time: Option<f64>,
}
impl Backend {
pub fn new(canvas_id: &str, run_mode: RunMode) -> Result<Backend, JsValue> {
let ctx = egui::Context::new();
load_memory(&ctx);
Ok(Backend {
ctx,
painter: webgl::Painter::new(canvas_id)?,
frame_times: egui::MovementTracker::new(1000, 1.0),
frame_start: None,
run_mode,
last_save_time: None,
})
}
pub fn run_mode(&self) -> RunMode {
self.run_mode
}
pub fn set_run_mode(&mut self, run_mode: RunMode) {
self.run_mode = run_mode;
}
/// id of the canvas html element containing the rendering
pub fn canvas_id(&self) -> &str {
self.painter.canvas_id()
}
/// Returns a master fullscreen UI, covering the entire screen.
pub fn begin_frame(&mut self, raw_input: egui::RawInput) -> egui::Ui {
self.frame_start = Some(now_sec());
self.ctx.begin_frame(raw_input)
}
pub fn end_frame(&mut self) -> Result<(egui::Output, egui::PaintJobs), JsValue> {
let frame_start = self
.frame_start
.take()
.expect("unmatched calls to begin_frame/end_frame");
let (output, paint_jobs) = self.ctx.end_frame();
self.auto_save();
let now = now_sec();
self.frame_times.add(now, (now - frame_start) as f32);
Ok((output, paint_jobs))
}
pub fn paint(&mut self, paint_jobs: egui::PaintJobs) -> Result<(), JsValue> {
let bg_color = egui::color::TRANSPARENT; // Use background css color.
self.painter.paint_jobs(
bg_color,
paint_jobs,
self.ctx.texture(),
self.ctx.pixels_per_point(),
)
}
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 {
self.painter.debug_info()
}
/// excludes painting
pub fn cpu_time(&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()
}
}
// ----------------------------------------------------------------------------
// TODO: Just use RawInput?
/// Data gathered between frames.
/// Is translated to `egui::RawInput` at the start of each frame.
#[derive(Default)]
pub struct WebInput {
pub mouse_pos: Option<egui::Pos2>,
pub mouse_down: bool, // TODO: which button
pub is_touch: bool,
pub scroll_delta: egui::Vec2,
pub events: Vec<egui::Event>,
}
impl WebInput {
pub fn new_frame(&mut self) -> egui::RawInput {
egui::RawInput {
mouse_down: self.mouse_down,
mouse_pos: self.mouse_pos,
scroll_delta: std::mem::take(&mut self.scroll_delta),
screen_size: screen_size().unwrap(),
pixels_per_point: Some(pixels_per_point()),
time: now_sec(),
seconds_since_midnight: Some(seconds_since_midnight()),
events: std::mem::take(&mut self.events),
}
}
}
// ----------------------------------------------------------------------------
pub struct AppRunner {
pub backend: Backend,
pub web_input: WebInput,
pub app: Box<dyn App>,
pub needs_repaint: bool, // TODO: move
}
impl AppRunner {
pub fn new(backend: Backend, app: Box<dyn App>) -> Result<Self, JsValue> {
Ok(Self {
backend,
web_input: Default::default(),
app,
needs_repaint: true, // TODO: move
})
}
pub fn canvas_id(&self) -> &str {
self.backend.canvas_id()
}
pub fn logic(&mut self) -> Result<(egui::Output, egui::PaintJobs), JsValue> {
resize_to_screen_size(self.backend.canvas_id());
let raw_input = self.web_input.new_frame();
let info = WebInfo {
web_location_hash: location_hash().unwrap_or_default(),
};
let mut ui = self.backend.begin_frame(raw_input);
self.app.ui(&mut ui, &mut self.backend, &info);
let (output, paint_jobs) = self.backend.end_frame()?;
handle_output(&output);
Ok((output, paint_jobs))
}
pub fn paint(&mut self, paint_jobs: egui::PaintJobs) -> Result<(), JsValue> {
self.backend.paint(paint_jobs)
}
}
/// Install event listeners to register different input events
/// and starts running the given `AppRunner`.
pub fn run(app_runner: AppRunner) -> Result<AppRunnerRef, JsValue> {
let runner_ref = AppRunnerRef(Arc::new(Mutex::new(app_runner)));
install_canvas_events(&runner_ref)?;
install_document_events(&runner_ref)?;
paint_and_schedule(runner_ref.clone())?;
Ok(runner_ref)
}
// ----------------------------------------------------------------------------
// Helpers to hide some of the verbosity of web_sys
@ -412,7 +216,7 @@ pub struct AppRunnerRef(Arc<Mutex<AppRunner>>);
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.backend.run_mode() == RunMode::Continuous || runner_lock.needs_repaint {
if runner_lock.web_backend.run_mode() == RunMode::Continuous || runner_lock.needs_repaint {
runner_lock.needs_repaint = false;
let (output, paint_jobs) = runner_lock.logic()?;
runner_lock.paint(paint_jobs)?;

View file

@ -1,6 +1,4 @@
use egui_glium::{persistence::Persistence, RunMode, Runner};
const APP_KEY: &str = "app";
use egui_glium::{storage::FileStorage, RunMode};
/// We dervive Deserialize/Serialize so we can persist app state on shutdown.
#[derive(Default, serde::Deserialize, serde::Serialize)]
@ -8,10 +6,10 @@ struct MyApp {
counter: u64,
}
impl egui_glium::App for MyApp {
impl egui::app::App for MyApp {
/// This function will be called whenever the Ui needs to be shown,
/// which may be many times per second.
fn ui(&mut self, ui: &mut egui::Ui, _: &mut Runner) {
fn ui(&mut self, ui: &mut egui::Ui, _: &mut dyn egui::app::Backend) {
if ui.button("Increment").clicked {
self.counter += 1;
}
@ -21,14 +19,14 @@ impl egui_glium::App for MyApp {
ui.label(format!("Counter: {}", self.counter));
}
fn on_exit(&mut self, persistence: &mut Persistence) {
persistence.set_value(APP_KEY, self); // Save app state
fn on_exit(&mut self, storage: &mut dyn egui::app::Storage) {
egui::app::set_value(storage, egui::app::APP_KEY, self);
}
}
fn main() {
let title = "My Egui Window";
let persistence = Persistence::from_path(".egui_example_glium.json".into()); // Where to persist app state
let app: MyApp = persistence.get_value(APP_KEY).unwrap_or_default(); // Restore `MyApp` from file, or create new `MyApp`.
egui_glium::run(title, RunMode::Reactive, persistence, app);
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);
}