diff --git a/Cargo.lock b/Cargo.lock index 016f841c..4bf96896 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -419,9 +419,12 @@ version = "0.1.0" dependencies = [ "egui 0.1.2", "egui_web 0.1.0", + "js-sys 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.11.0 (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)", "wasm-bindgen 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "web-sys 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] diff --git a/docs/example_wasm.js b/docs/example_wasm.js index 3fd45398..8b5a4b59 100644 --- a/docs/example_wasm.js +++ b/docs/example_wasm.js @@ -187,41 +187,59 @@ function passStringToWasm0(arg, malloc, realloc) { WASM_VECTOR_LEN = offset; return ptr; } -/** -* @param {string} canvas_id -* @returns {State} -*/ -__exports.new_webgl_gui = function(canvas_id) { - var ptr0 = passStringToWasm0(canvas_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len0 = WASM_VECTOR_LEN; - var ret = wasm.new_webgl_gui(ptr0, len0); - return State.__wrap(ret); -}; -/** -* @param {string} canvas_id -*/ -__exports.resize_to_screen_size = function(canvas_id) { - var ptr0 = passStringToWasm0(canvas_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len0 = WASM_VECTOR_LEN; - wasm.resize_to_screen_size(ptr0, len0); -}; - -function _assertClass(instance, klass) { - if (!(instance instanceof klass)) { - throw new Error(`expected instance of ${klass.name}`); - } - return instance.ptr; +function makeMutClosure(arg0, arg1, dtor, f) { + const state = { a: arg0, b: arg1, cnt: 1 }; + const real = (...args) => { + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + const a = state.a; + state.a = 0; + try { + return f(a, state.b, ...args); + } finally { + if (--state.cnt === 0) wasm.__wbindgen_export_2.get(dtor)(a, state.b); + else state.a = a; + } + }; + real.original = state; + 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__h59314522206fe5c1(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__h59314522206fe5c1(arg0, arg1, addHeapObject(arg2)); +} + +function __wbg_adapter_32(arg0, arg1, arg2) { + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h59314522206fe5c1(arg0, arg1, addHeapObject(arg2)); +} + +function __wbg_adapter_35(arg0, arg1) { + wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h352b9cbf82e6c0ee(arg0, arg1); +} + +function __wbg_adapter_38(arg0, arg1) { + wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h1322780ee0c72b2c(arg0, arg1); +} + +function __wbg_adapter_41(arg0, arg1, arg2) { + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h59314522206fe5c1(arg0, arg1, addHeapObject(arg2)); +} + /** -* @param {State} state -* @param {string} web_input_json +* @param {string} canvas_id +* @returns {StateRef} */ -__exports.run_gui = function(state, web_input_json) { - _assertClass(state, State); - var ptr0 = passStringToWasm0(web_input_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); +__exports.start = function(canvas_id) { + var ptr0 = passStringToWasm0(canvas_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); var len0 = WASM_VECTOR_LEN; - wasm.run_gui(state.ptr, ptr0, len0); + var ret = wasm.start(ptr0, len0); + return StateRef.__wrap(ret); }; function handleError(f) { @@ -240,10 +258,10 @@ function getArrayU8FromWasm0(ptr, len) { } /** */ -class State { +class StateRef { static __wrap(ptr) { - const obj = Object.create(State.prototype); + const obj = Object.create(StateRef.prototype); obj.ptr = ptr; return obj; @@ -253,10 +271,10 @@ class State { const ptr = this.ptr; this.ptr = 0; - wasm.__wbg_state_free(ptr); + wasm.__wbg_stateref_free(ptr); } } -__exports.State = State; +__exports.StateRef = StateRef; async function load(module, imports) { if (typeof Response === 'function' && module instanceof Response) { @@ -306,6 +324,18 @@ async function init(input) { imports.wbg.__wbindgen_object_drop_ref = function(arg0) { takeObject(arg0); }; + imports.wbg.__wbindgen_cb_forget = function(arg0) { + takeObject(arg0); + }; + imports.wbg.__wbindgen_cb_drop = function(arg0) { + const obj = takeObject(arg0).original; + if (obj.cnt-- == 1) { + obj.a = 0; + return true; + } + var ret = false; + return ret; + }; imports.wbg.__wbindgen_string_new = function(arg0, arg1) { var ret = getStringFromWasm0(arg0, arg1); return addHeapObject(ret); @@ -346,6 +376,10 @@ async function init(input) { var ret = getObject(arg0).open(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); return isLikeNone(ret) ? 0 : addHeapObject(ret); }); + imports.wbg.__wbg_requestAnimationFrame_a18bbc3e00b14f1d = handleError(function(arg0, arg1) { + var ret = getObject(arg0).requestAnimationFrame(getObject(arg1)); + return ret; + }); imports.wbg.__wbg_body_ca5c7fc933a74206 = function(arg0) { var ret = getObject(arg0).body; return isLikeNone(ret) ? 0 : addHeapObject(ret); @@ -354,6 +388,35 @@ async function init(input) { var ret = getObject(arg0).getElementById(getStringFromWasm0(arg1, arg2)); return isLikeNone(ret) ? 0 : addHeapObject(ret); }; + imports.wbg.__wbg_touches_b43aff7721e20859 = function(arg0) { + var ret = getObject(arg0).touches; + return addHeapObject(ret); + }; + imports.wbg.__wbg_get_6d1471ca01108161 = function(arg0, arg1) { + var ret = getObject(arg0)[arg1 >>> 0]; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }; + imports.wbg.__wbg_top_4bce526516916bef = function(arg0) { + var ret = getObject(arg0).top; + return ret; + }; + imports.wbg.__wbg_left_ef807cf36a8b9eff = function(arg0) { + var ret = getObject(arg0).left; + return ret; + }; + imports.wbg.__wbg_hash_807752e2195bb755 = handleError(function(arg0, arg1) { + var ret = getObject(arg1).hash; + var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }); + imports.wbg.__wbg_setProperty_3090dd7650e67dd9 = handleError(function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).setProperty(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); + }); + imports.wbg.__wbg_addEventListener_e26664ded96802d6 = handleError(function(arg0, arg1, arg2, arg3) { + getObject(arg0).addEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3)); + }); imports.wbg.__wbg_getItem_5a53c566950c2181 = handleError(function(arg0, arg1, arg2, arg3) { var ret = getObject(arg1).getItem(getStringFromWasm0(arg2, arg3)); var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); @@ -364,9 +427,18 @@ async function init(input) { imports.wbg.__wbg_setItem_6884e4d52a076b78 = handleError(function(arg0, arg1, arg2, arg3, arg4) { getObject(arg0).setItem(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); }); - imports.wbg.__wbg_setProperty_3090dd7650e67dd9 = handleError(function(arg0, arg1, arg2, arg3, arg4) { - getObject(arg0).setProperty(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); - }); + imports.wbg.__wbg_deltaX_8e512069555e35b9 = function(arg0) { + var ret = getObject(arg0).deltaX; + return ret; + }; + imports.wbg.__wbg_deltaY_2ecd7349fda0247d = function(arg0) { + var ret = getObject(arg0).deltaY; + return ret; + }; + imports.wbg.__wbg_getBoundingClientRect_87b5d0eaec291192 = function(arg0) { + var ret = getObject(arg0).getBoundingClientRect(); + return addHeapObject(ret); + }; imports.wbg.__wbg_instanceof_WebGlRenderingContext_4dbaf147f8c72285 = function(arg0) { var ret = getObject(arg0) instanceof WebGLRenderingContext; return ret; @@ -498,16 +570,38 @@ async function init(input) { var ret = getObject(arg0).style; return addHeapObject(ret); }; - imports.wbg.__wbg_hash_807752e2195bb755 = handleError(function(arg0, arg1) { - var ret = getObject(arg1).hash; + imports.wbg.__wbg_clientX_c98258465ff0d711 = function(arg0) { + var ret = getObject(arg0).clientX; + return ret; + }; + imports.wbg.__wbg_clientY_44f939bc3df4dbd5 = function(arg0) { + var ret = getObject(arg0).clientY; + return ret; + }; + imports.wbg.__wbg_now_66c779566d9324ca = function(arg0) { + var ret = getObject(arg0).now(); + return ret; + }; + imports.wbg.__wbg_pageX_6a171f721f972234 = function(arg0) { + var ret = getObject(arg0).pageX; + return ret; + }; + imports.wbg.__wbg_pageY_01ba10e48716178f = function(arg0) { + var ret = getObject(arg0).pageY; + return ret; + }; + imports.wbg.__wbg_key_0d736f3a5f2ca002 = function(arg0, arg1) { + var ret = getObject(arg1).key; var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); var len0 = WASM_VECTOR_LEN; getInt32Memory0()[arg0 / 4 + 1] = len0; getInt32Memory0()[arg0 / 4 + 0] = ptr0; - }); - imports.wbg.__wbg_now_66c779566d9324ca = function(arg0) { - var ret = getObject(arg0).now(); - return ret; + }; + imports.wbg.__wbg_preventDefault_cb207bbb7569acb3 = function(arg0) { + getObject(arg0).preventDefault(); + }; + imports.wbg.__wbg_stopPropagation_2c1e23fe8563d714 = function(arg0) { + getObject(arg0).stopPropagation(); }; imports.wbg.__wbg_instanceof_HtmlCanvasElement_0d5b3d4264830667 = function(arg0) { var ret = getObject(arg0) instanceof HTMLCanvasElement; @@ -543,6 +637,26 @@ async function init(input) { var ret = new Function(getStringFromWasm0(arg0, arg1)); return addHeapObject(ret); }; + imports.wbg.__wbg_getHours_ca3bbac0f3d04bd6 = function(arg0) { + var ret = getObject(arg0).getHours(); + return ret; + }; + imports.wbg.__wbg_getMilliseconds_a7841eba602b6404 = function(arg0) { + var ret = getObject(arg0).getMilliseconds(); + return ret; + }; + imports.wbg.__wbg_getMinutes_7086fbbe31709de7 = function(arg0) { + var ret = getObject(arg0).getMinutes(); + return ret; + }; + imports.wbg.__wbg_getSeconds_bb43691d4ad9e1ec = function(arg0) { + var ret = getObject(arg0).getSeconds(); + return ret; + }; + imports.wbg.__wbg_new0_8d817915cd890bd8 = function() { + var ret = new Date(); + return addHeapObject(ret); + }; imports.wbg.__wbg_self_d1b58dbab69d5bb1 = handleError(function() { var ret = self.self; return addHeapObject(ret); @@ -631,6 +745,30 @@ async function init(input) { var ret = wasm.memory; return addHeapObject(ret); }; + imports.wbg.__wbindgen_closure_wrapper176 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 38, __wbg_adapter_35); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper170 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 38, __wbg_adapter_26); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper166 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 38, __wbg_adapter_38); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper172 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 38, __wbg_adapter_41); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper168 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 38, __wbg_adapter_29); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_closure_wrapper174 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 38, __wbg_adapter_32); + return addHeapObject(ret); + }; if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { input = fetch(input); diff --git a/docs/example_wasm_bg.wasm b/docs/example_wasm_bg.wasm index c2ff210c..4757512c 100644 Binary files a/docs/example_wasm_bg.wasm and b/docs/example_wasm_bg.wasm differ diff --git a/docs/index.html b/docs/index.html index 72b5a545..216e8590 100644 --- a/docs/index.html +++ b/docs/index.html @@ -30,8 +30,8 @@ - - + + diff --git a/egui_web/Cargo.toml b/egui_web/Cargo.toml index 64bccb99..08785b0e 100644 --- a/egui_web/Cargo.toml +++ b/egui_web/Cargo.toml @@ -22,17 +22,22 @@ features = [ 'console', 'CssStyleDeclaration', 'Document', + 'DomRect', 'Element', 'HtmlCanvasElement', 'HtmlElement', 'Location', 'Performance', 'Storage', + 'Touch', + 'TouchEvent', + 'TouchList', 'WebGlBuffer', 'WebGlProgram', 'WebGlRenderingContext', 'WebGlShader', 'WebGlTexture', 'WebGlUniformLocation', + 'WheelEvent', 'Window', ] diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index 3f183dc7..9c7bfb62 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -8,18 +8,18 @@ pub mod webgl; use std::sync::Arc; use wasm_bindgen::JsValue; -pub struct State { +pub struct EguiWeb { ctx: Arc, webgl_painter: webgl::Painter, frame_times: egui::MovementTracker, frame_start: Option, } -impl State { - pub fn new(canvas_id: &str) -> Result { +impl EguiWeb { + pub fn new(canvas_id: &str) -> Result { let ctx = egui::Context::new(); load_memory(&ctx); - Ok(State { + Ok(EguiWeb { ctx, webgl_painter: webgl::Painter::new(canvas_id)?, frame_times: egui::MovementTracker::new(1000, 1.0), @@ -27,6 +27,11 @@ impl State { }) } + /// id of the canvas html element containing the rendering + pub fn canvas_id(&self) -> &str { + self.webgl_painter.canvas_id() + } + pub fn begin_frame(&mut self, raw_input: egui::RawInput) -> egui::Ui { self.frame_start = Some(now_sec()); self.ctx.begin_frame(raw_input) @@ -109,12 +114,37 @@ pub fn pixels_per_point() -> f32 { } } -pub fn resize_to_screen_size(canvas_id: &str) -> Option<()> { +pub fn canvas_element(canvas_id: &str) -> Option { use wasm_bindgen::JsCast; let document = web_sys::window()?.document()?; let canvas = document.get_element_by_id(canvas_id)?; - let canvas: web_sys::HtmlCanvasElement = - canvas.dyn_into::().ok()?; + canvas.dyn_into::().ok() +} + +pub fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement { + crate::canvas_element(canvas_id) + .unwrap_or_else(|| panic!("Failed to find canvas with id '{}'", canvas_id)) +} + +pub fn pos_from_mouse_event(canvas_id: &str, event: &web_sys::MouseEvent) -> egui::Pos2 { + let canvas = canvas_element(canvas_id).unwrap(); + let rect = canvas.get_bounding_client_rect(); + egui::Pos2 { + x: event.client_x() as f32 - rect.left() as f32, + y: event.client_y() as f32 - rect.top() as f32, + } +} + +pub fn pos_from_touch_event(event: &web_sys::TouchEvent) -> egui::Pos2 { + let t = event.touches().get(0).unwrap(); + egui::Pos2 { + x: t.page_x() as f32, + y: t.page_y() as f32, + } +} + +pub fn resize_to_screen_size(canvas_id: &str) -> Option<()> { + let canvas = canvas_element(canvas_id)?; let screen_size = screen_size()?; let pixels_per_point = pixels_per_point(); @@ -218,3 +248,27 @@ pub fn open_url(url: &str) -> Option<()> { pub fn location_hash() -> Option { web_sys::window()?.location().hash().ok() } + +pub fn translate_key(key: &str) -> Option { + match key { + "Alt" => Some(egui::Key::Alt), + "Backspace" => Some(egui::Key::Backspace), + "Control" => Some(egui::Key::Control), + "Delete" => Some(egui::Key::Delete), + "ArrowDown" => Some(egui::Key::Down), + "End" => Some(egui::Key::End), + "Escape" => Some(egui::Key::Escape), + "Home" => Some(egui::Key::Home), + "Help" => Some(egui::Key::Insert), + "ArrowLeft" => Some(egui::Key::Left), + "Meta" => Some(egui::Key::Logo), + "PageDown" => Some(egui::Key::PageDown), + "PageUp" => Some(egui::Key::PageUp), + "Enter" => Some(egui::Key::Return), + "ArrowRight" => Some(egui::Key::Right), + "Shift" => Some(egui::Key::Shift), + "Tab" => Some(egui::Key::Tab), + "ArrowUp" => Some(egui::Key::Up), + _ => None, + } +} diff --git a/egui_web/src/webgl.rs b/egui_web/src/webgl.rs index 0c69b1ee..84c3bb83 100644 --- a/egui_web/src/webgl.rs +++ b/egui_web/src/webgl.rs @@ -13,6 +13,7 @@ use egui::{ type Gl = WebGlRenderingContext; pub struct Painter { + canvas_id: String, canvas: web_sys::HtmlCanvasElement, gl: WebGlRenderingContext, texture: WebGlTexture, @@ -38,9 +39,7 @@ impl Painter { } pub fn new(canvas_id: &str) -> Result { - let document = web_sys::window().unwrap().document().unwrap(); - let canvas = document.get_element_by_id(canvas_id).unwrap(); - let canvas: web_sys::HtmlCanvasElement = canvas.dyn_into::()?; + let canvas = crate::canvas_element_or_die(canvas_id); let gl = canvas .get_context("webgl")? @@ -101,6 +100,7 @@ impl Painter { let color_buffer = gl.create_buffer().ok_or("failed to create color_buffer")?; Ok(Painter { + canvas_id: canvas_id.to_owned(), canvas, gl, texture: gl_texture, @@ -114,6 +114,11 @@ impl Painter { }) } + /// id of the canvas html element containing the rendering + pub fn canvas_id(&self) -> &str { + &self.canvas_id + } + fn upload_texture(&mut self, texture: &Texture) { if self.current_texture_id == Some(texture.id) { return; // No change diff --git a/example_wasm/Cargo.toml b/example_wasm/Cargo.toml index a81dd12b..bf4c7ba7 100644 --- a/example_wasm/Cargo.toml +++ b/example_wasm/Cargo.toml @@ -9,9 +9,34 @@ edition = "2018" crate-type = ["cdylib", "rlib"] [dependencies] +js-sys = "0.3" +parking_lot = "0.11" serde = { version = "1", features = ["derive"] } serde_json = "1" wasm-bindgen = "0.2" egui = { path = "../egui" } egui_web = { path = "../egui_web" } + +[dependencies.web-sys] +version = "0.3" +features = [ + 'console', + 'CssStyleDeclaration', + 'Document', + 'Element', + 'HtmlCanvasElement', + 'HtmlElement', + 'KeyboardEvent', + 'Location', + 'MouseEvent', + 'Performance', + 'Storage', + 'WebGlBuffer', + 'WebGlProgram', + 'WebGlRenderingContext', + 'WebGlShader', + 'WebGlTexture', + 'WebGlUniformLocation', + 'Window', +] diff --git a/example_wasm/src/lib.rs b/example_wasm/src/lib.rs index 2ca2dbf3..81e6280e 100644 --- a/example_wasm/src/lib.rs +++ b/example_wasm/src/lib.rs @@ -5,20 +5,56 @@ use egui::{examples::ExampleApp, label, widgets::Separator, Align, RawInput, Tex use wasm_bindgen::prelude::*; -#[wasm_bindgen] +// ---------------------------------------------------------------------------- + +/// Data gathered between frames. +/// Is translated to `egui::RawInput` at the start of each frame. +#[derive(Default)] +pub struct WebInput { + pub mouse_pos: Option, + pub mouse_down: bool, // TODO: which button + pub is_touch: bool, + pub scroll_delta: Vec2, + pub events: Vec, +} + +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: egui_web::screen_size().unwrap(), + pixels_per_point: Some(egui_web::pixels_per_point()), + time: egui_web::now_sec(), + seconds_since_midnight: Some(egui_web::seconds_since_midnight()), + events: std::mem::take(&mut self.events), + } + } +} + +// ---------------------------------------------------------------------------- + pub struct State { - egui_web: egui_web::State, + egui_web: egui_web::EguiWeb, + web_input: WebInput, example_app: ExampleApp, } impl State { fn new(canvas_id: &str) -> Result { Ok(State { - egui_web: egui_web::State::new(canvas_id)?, + egui_web: egui_web::EguiWeb::new(canvas_id)?, + web_input: Default::default(), example_app: Default::default(), }) } + /// id of the canvas html element containing the rendering + pub fn canvas_id(&self) -> &str { + self.egui_web.canvas_id() + } + fn run(&mut self, raw_input: RawInput, web_location_hash: &str) -> Result { let mut ui = self.egui_web.begin_frame(raw_input); self.ui(&mut ui, web_location_hash); @@ -58,22 +94,252 @@ impl State { } } -#[wasm_bindgen] -pub fn new_webgl_gui(canvas_id: &str) -> Result { - State::new(canvas_id) -} +// ---------------------------------------------------------------------------- +use parking_lot::Mutex; +use std::sync::Arc; #[wasm_bindgen] -pub fn resize_to_screen_size(canvas_id: &str) { - egui_web::resize_to_screen_size(canvas_id); -} +#[derive(Clone)] +pub struct StateRef(Arc>); + +/// If true, paint at full framerate always. +/// If false, only paint on input. +/// TODO: if this is turned off we must turn off animations too (which hasn't been implemented yet). +const ANIMATION_FRAME: bool = true; #[wasm_bindgen] -pub fn run_gui(state: &mut State, web_input_json: &str) -> Result<(), JsValue> { - // TODO: nicer interface than JSON - let raw_input: RawInput = serde_json::from_str(web_input_json).unwrap(); +pub fn start(canvas_id: &str) -> Result { + let state = State::new(canvas_id)?; + let state = StateRef(Arc::new(Mutex::new(state))); + + install_canvas_events(&state)?; + install_document_events(&state)?; + paint_and_schedule(state.clone())?; + + Ok(state) +} + +fn paint_and_schedule(state: StateRef) -> Result<(), JsValue> { + paint(&mut state.0.lock())?; + if ANIMATION_FRAME { + request_animation_frame(state)?; + } + Ok(()) +} + +fn request_animation_frame(state: StateRef) -> Result<(), JsValue> { + use wasm_bindgen::JsCast; + let window = web_sys::window().unwrap(); + let closure = Closure::once(move || paint_and_schedule(state)); + window.request_animation_frame(closure.as_ref().unchecked_ref())?; + closure.forget(); // We must forget it, or else the callback is canceled on drop + Ok(()) +} + +fn paint(state: &mut State) -> Result<(), JsValue> { + egui_web::resize_to_screen_size(state.canvas_id()); + let raw_input = state.web_input.new_frame(); let web_location_hash = egui_web::location_hash().unwrap_or_default(); let output = state.run(raw_input, &web_location_hash)?; egui_web::handle_output(&output); Ok(()) } + +fn invalidate(state: &mut State) -> Result<(), JsValue> { + if ANIMATION_FRAME { + Ok(()) // No need to invalidate - we repaint all the time + } else { + paint(state) // TODO: schedule repaint instead? + } +} + +fn install_document_events(state: &StateRef) -> Result<(), JsValue> { + use wasm_bindgen::JsCast; + let document = web_sys::window().unwrap().document().unwrap(); + + { + // keydown + let state = state.clone(); + let closure = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| { + let mut state = state.0.lock(); + let key = event.key(); + if let Some(key) = egui_web::translate_key(&key) { + state + .web_input + .events + .push(Event::Key { key, pressed: true }); + } else { + state.web_input.events.push(Event::Text(key)); + } + invalidate(&mut state).unwrap(); + }) as Box); + document.add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())?; + closure.forget(); + } + + { + // keyup + let state = state.clone(); + let closure = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| { + let mut state = state.0.lock(); + let key = event.key(); + if let Some(key) = egui_web::translate_key(&key) { + state.web_input.events.push(Event::Key { + key, + pressed: false, + }); + invalidate(&mut state).unwrap(); + } + }) as Box); + document.add_event_listener_with_callback("keyup", closure.as_ref().unchecked_ref())?; + closure.forget(); + } + + for event_name in &["load", "pagehide", "pageshow", "resize"] { + let state = state.clone(); + let closure = Closure::wrap(Box::new(move || { + invalidate(&mut state.0.lock()).unwrap(); + }) as Box); + document.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; + closure.forget(); + } + + Ok(()) +} + +fn install_canvas_events(state: &StateRef) -> Result<(), JsValue> { + use egui_web::pos_from_mouse_event; + use wasm_bindgen::JsCast; + let canvas = egui_web::canvas_element(state.0.lock().canvas_id()).unwrap(); + + { + let event_name = "mousedown"; + let state = state.clone(); + let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { + let mut state = state.0.lock(); + if !state.web_input.is_touch { + state.web_input.mouse_pos = Some(pos_from_mouse_event(state.canvas_id(), &event)); + state.web_input.mouse_down = true; + invalidate(&mut state).unwrap(); + event.stop_propagation(); + event.prevent_default(); + } + }) as Box); + canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; + closure.forget(); + } + + { + let event_name = "mousemove"; + let state = state.clone(); + let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { + let mut state = state.0.lock(); + if !state.web_input.is_touch { + state.web_input.mouse_pos = Some(pos_from_mouse_event(state.canvas_id(), &event)); + invalidate(&mut state).unwrap(); + event.stop_propagation(); + event.prevent_default(); + } + }) as Box); + canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; + closure.forget(); + } + + { + let event_name = "mouseup"; + let state = state.clone(); + let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { + let mut state = state.0.lock(); + if !state.web_input.is_touch { + state.web_input.mouse_pos = Some(pos_from_mouse_event(state.canvas_id(), &event)); + state.web_input.mouse_down = false; + invalidate(&mut state).unwrap(); + event.stop_propagation(); + event.prevent_default(); + } + }) as Box); + canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; + closure.forget(); + } + + { + let event_name = "mouseleave"; + let state = state.clone(); + let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| { + let mut state = state.0.lock(); + if !state.web_input.is_touch { + state.web_input.mouse_pos = None; + invalidate(&mut state).unwrap(); + event.stop_propagation(); + event.prevent_default(); + } + }) as Box); + canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; + closure.forget(); + } + + { + let event_name = "touchstart"; + let state = state.clone(); + let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| { + let mut state = state.0.lock(); + state.web_input.is_touch = true; + state.web_input.mouse_pos = Some(egui_web::pos_from_touch_event(&event)); + state.web_input.mouse_down = true; + invalidate(&mut state).unwrap(); + event.stop_propagation(); + event.prevent_default(); + }) as Box); + canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; + closure.forget(); + } + + { + let event_name = "touchmove"; + let state = state.clone(); + let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| { + let mut state = state.0.lock(); + state.web_input.is_touch = true; + state.web_input.mouse_pos = Some(egui_web::pos_from_touch_event(&event)); + invalidate(&mut state).unwrap(); + event.stop_propagation(); + event.prevent_default(); + }) as Box); + canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; + closure.forget(); + } + + { + let event_name = "touchend"; + let state = state.clone(); + let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| { + let mut state = state.0.lock(); + state.web_input.is_touch = true; + state.web_input.mouse_down = false; // First release mouse to click... + paint(&mut state).unwrap(); // ...do the clicking... + state.web_input.mouse_pos = None; // ...remove hover effect + invalidate(&mut state).unwrap(); + event.stop_propagation(); + event.prevent_default(); + }) as Box); + canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; + closure.forget(); + } + + { + let event_name = "wheel"; + let state = state.clone(); + let closure = Closure::wrap(Box::new(move |event: web_sys::WheelEvent| { + let mut state = state.0.lock(); + state.web_input.scroll_delta.x -= event.delta_x() as f32; + state.web_input.scroll_delta.y -= event.delta_y() as f32; + invalidate(&mut state).unwrap(); + event.stop_propagation(); + event.prevent_default(); + }) as Box); + canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; + closure.forget(); + } + + Ok(()) +}