diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..1d09522cdd5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.sublime* +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000000..787860ae3fc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "emgui" +version = "0.1.0" +authors = ["Emil Ernerfeldt "] + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +# rand = { version="0.6", features = ['wasm-bindgen'] } +serde = "1" +serde_derive = "1" +serde_json = "1" +wasm-bindgen = "0.2" +web-sys = { version = "0.3.5", features = ['console', 'Performance', 'Window'] } + +# Optimize for small code size: +[profile.dev] +opt-level = "s" + +[profile.release] +opt-level = "s" diff --git a/README.md b/README.md new file mode 100644 index 00000000000..6d134d20add --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Emgui – An Experimental, Modularized immediate mode Graphical User Interface + +Here are the steps, in chronological order of execution: + +CODE: Input bindings, i.e. gathering GuiInput data from the system (web browser, Mac Window, iPhone App, ...) +DATA: GuiInput: mouse and keyboard state + window size +DATA: GuiSizes: this is a configuration of the ImLayout system, sets sizes of e.g. a slider. +CODE: ImLayout: Immediate mode layout Gui elements. THIS IS WHAT YOUR APP CODE CALLS! +DATA: GuiPaint: High-level commands to render e.g. a checked box with a hover-effect at a certain position. +DATA: GuiStyle: The colors/shading of the gui. +CODE: GuiPainter: Renders GuiPaint + GuiStyle into DrawCommands +DATA: PaintCommands: low-level commands (e.g. "Draw a rectangle with this color here") +CODE: Painter: paints the the PaintCommands to the screen (HTML canvas, OpenGL, ...) + +This is similar to Dear ImGui but separates the layout from the rendering, and adds another step to the rendering. + +# Implementation + +Input is gathered in TypeScript. +PaintCommands rendered to a HTML canvas. +Everything else is written in Rust, compiled to WASM. diff --git a/build.sh b/build.sh new file mode 100755 index 00000000000..828efa0afa9 --- /dev/null +++ b/build.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -eu + +# Pre-requisites: +rustup target add wasm32-unknown-unknown +if ! [[ $(wasm-bindgen --version) ]]; then + cargo install wasm-bindgen-cli +fi + +BUILD=debug +# BUILD=release + +# Clear output from old stuff: +rm -rf docs/*.d.ts +rm -rf docs/*.js +rm -rf docs/*.wasm + +echo "Build rust:" +cargo build --target wasm32-unknown-unknown + +echo "Lint and clean up typescript:" +tslint --fix docs/*.ts + +echo "Compile typescript:" +tsc + +echo "Generate JS bindings for wasm:" + +FOLDER_NAME=${PWD##*/} +TARGET_NAME="$FOLDER_NAME.wasm" +wasm-bindgen "target/wasm32-unknown-unknown/$BUILD/$TARGET_NAME" \ + --out-dir docs --no-modules + # --no-modules-global hoboho diff --git a/build_and_run.sh b/build_and_run.sh new file mode 100755 index 00000000000..ea1e91e0522 --- /dev/null +++ b/build_and_run.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -eu + +./build.sh + +open "docs/index.html" diff --git a/docs/emgui.d.ts b/docs/emgui.d.ts new file mode 100644 index 00000000000..7eec4b075f0 --- /dev/null +++ b/docs/emgui.d.ts @@ -0,0 +1,11 @@ +/* tslint:disable */ +export function show_gui(arg0: string): string; + +export class Input { +free(): void; +screen_width: number +screen_height: number +mouse_x: number +mouse_y: number + +} diff --git a/docs/emgui.js b/docs/emgui.js new file mode 100644 index 00000000000..eec87cd10d1 --- /dev/null +++ b/docs/emgui.js @@ -0,0 +1,151 @@ +(function() { + var wasm; + const __exports = {}; + + + let cachedTextEncoder = new TextEncoder('utf-8'); + + let cachegetUint8Memory = null; + function getUint8Memory() { + if (cachegetUint8Memory === null || cachegetUint8Memory.buffer !== wasm.memory.buffer) { + cachegetUint8Memory = new Uint8Array(wasm.memory.buffer); + } + return cachegetUint8Memory; + } + + function passStringToWasm(arg) { + + const buf = cachedTextEncoder.encode(arg); + const ptr = wasm.__wbindgen_malloc(buf.length); + getUint8Memory().set(buf, ptr); + return [ptr, buf.length]; + } + + let cachedTextDecoder = new TextDecoder('utf-8'); + + function getStringFromWasm(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory().subarray(ptr, ptr + len)); + } + + let cachedGlobalArgumentPtr = null; + function globalArgumentPtr() { + if (cachedGlobalArgumentPtr === null) { + cachedGlobalArgumentPtr = wasm.__wbindgen_global_argument_ptr(); + } + return cachedGlobalArgumentPtr; + } + + let cachegetUint32Memory = null; + function getUint32Memory() { + if (cachegetUint32Memory === null || cachegetUint32Memory.buffer !== wasm.memory.buffer) { + cachegetUint32Memory = new Uint32Array(wasm.memory.buffer); + } + return cachegetUint32Memory; + } + /** + * @param {string} arg0 + * @returns {string} + */ + __exports.show_gui = function(arg0) { + const [ptr0, len0] = passStringToWasm(arg0); + const retptr = globalArgumentPtr(); + try { + wasm.show_gui(retptr, ptr0, len0); + const mem = getUint32Memory(); + const rustptr = mem[retptr / 4]; + const rustlen = mem[retptr / 4 + 1]; + + const realRet = getStringFromWasm(rustptr, rustlen).slice(); + wasm.__wbindgen_free(rustptr, rustlen * 1); + return realRet; + + + } finally { + wasm.__wbindgen_free(ptr0, len0 * 1); + + } + + }; + + function freeInput(ptr) { + + wasm.__wbg_input_free(ptr); + } + /** + */ + class Input { + + free() { + const ptr = this.ptr; + this.ptr = 0; + freeInput(ptr); + } + + /** + * @returns {number} + */ + get screen_width() { + return wasm.__wbg_get_input_screen_width(this.ptr); + } + set screen_width(arg0) { + return wasm.__wbg_set_input_screen_width(this.ptr, arg0); + } + /** + * @returns {number} + */ + get screen_height() { + return wasm.__wbg_get_input_screen_height(this.ptr); + } + set screen_height(arg0) { + return wasm.__wbg_set_input_screen_height(this.ptr, arg0); + } + /** + * @returns {number} + */ + get mouse_x() { + return wasm.__wbg_get_input_mouse_x(this.ptr); + } + set mouse_x(arg0) { + return wasm.__wbg_set_input_mouse_x(this.ptr, arg0); + } + /** + * @returns {number} + */ + get mouse_y() { + return wasm.__wbg_get_input_mouse_y(this.ptr); + } + set mouse_y(arg0) { + return wasm.__wbg_set_input_mouse_y(this.ptr, arg0); + } + } + __exports.Input = Input; + + __exports.__wbindgen_throw = function(ptr, len) { + throw new Error(getStringFromWasm(ptr, len)); + }; + + function init(path_or_module) { + let instantiation; + const imports = { './emgui': __exports }; + if (path_or_module instanceof WebAssembly.Module) { + instantiation = WebAssembly.instantiate(path_or_module, imports) + .then(instance => { + return { instance, module: module_or_path } + }); + } else { + const data = fetch(path_or_module); + if (typeof WebAssembly.instantiateStreaming === 'function') { + instantiation = WebAssembly.instantiateStreaming(data, imports); + } else { + instantiation = data + .then(response => response.arrayBuffer()) + .then(buffer => WebAssembly.instantiate(buffer, imports)); + } + } + return instantiation.then(({instance}) => { + wasm = init.wasm = instance.exports; + return; + }); +}; +self.wasm_bindgen = Object.assign(init, __exports); +})(); diff --git a/docs/emgui_bg.wasm b/docs/emgui_bg.wasm new file mode 100644 index 00000000000..526b6f0be55 Binary files /dev/null and b/docs/emgui_bg.wasm differ diff --git a/docs/frontend.js b/docs/frontend.js new file mode 100644 index 00000000000..381b2a49705 --- /dev/null +++ b/docs/frontend.js @@ -0,0 +1,112 @@ +// ---------------------------------------------------------------------------- +// Paint module: +function paintCommand(canvas, cmd) { + var ctx = canvas.getContext("2d"); + switch (cmd.kind) { + case "clear": + ctx.fillStyle = cmd.fill_style; + ctx.clearRect(0, 0, canvas.width, canvas.height); + return; + case "line": + ctx.beginPath(); + ctx.lineWidth = cmd.line_width; + ctx.strokeStyle = cmd.stroke_style; + ctx.moveTo(cmd.from[0], cmd.from[1]); + ctx.lineTo(cmd.to[0], cmd.to[1]); + ctx.stroke(); + return; + case "circle": + ctx.fillStyle = cmd.fill_style; + ctx.beginPath(); + ctx.arc(cmd.center[0], cmd.center[1], cmd.radius, 0, 2 * Math.PI, false); + ctx.fill(); + return; + case "rounded_rect": + ctx.fillStyle = cmd.fill_style; + var x = cmd.pos[0]; + var y = cmd.pos[1]; + var width = cmd.size[0]; + var height = cmd.size[1]; + var radius = cmd.radius; + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + ctx.fill(); + return; + case "text": + ctx.font = cmd.font; + ctx.fillStyle = cmd.fill_style; + ctx.textAlign = cmd.text_align; + ctx.fillText(cmd.text, cmd.pos[0], cmd.pos[1]); + return; + } +} +// we'll defer our execution until the wasm is ready to go +function wasm_loaded() { + console.log("wasm loaded"); + initialize(); +} +// here we tell bindgen the path to the wasm file so it can start +// initialization and return to us a promise when it's done +wasm_bindgen("./emgui_bg.wasm") + .then(wasm_loaded)["catch"](console.error); +function rust_gui(input) { + return JSON.parse(wasm_bindgen.show_gui(JSON.stringify(input))); +} +// ---------------------------------------------------------------------------- +function js_gui(input) { + var commands = []; + commands.push({ + fillStyle: "#111111", + kind: "clear" + }); + commands.push({ + fillStyle: "#ff1111", + kind: "rounded_rect", + pos: [100, 100], + radius: 20, + size: [200, 100] + }); + return commands; +} +function paint_gui(canvas, mouse_pos) { + var input = { + mouse_x: mouse_pos.x, + mouse_y: mouse_pos.y, + screen_height: canvas.height, + screen_width: canvas.width + }; + var commands = rust_gui(input); + for (var _i = 0, commands_1 = commands; _i < commands_1.length; _i++) { + var cmd = commands_1[_i]; + paintCommand(canvas, cmd); + } +} +// ---------------------------------------------------------------------------- +function mouse_pos_from_event(canvas, evt) { + var rect = canvas.getBoundingClientRect(); + return { + x: evt.clientX - rect.left, + y: evt.clientY - rect.top + }; +} +function initialize() { + var canvas = document.getElementById("canvas"); + canvas.addEventListener("mousemove", function (evt) { + var mouse_pos = mouse_pos_from_event(canvas, evt); + paint_gui(canvas, mouse_pos); + }, false); + canvas.addEventListener("mousedown", function (evt) { + var mouse_pos = mouse_pos_from_event(canvas, evt); + paint_gui(canvas, mouse_pos); + }, false); + paint_gui(canvas, { x: 0, y: 0 }); +} diff --git a/docs/frontend.ts b/docs/frontend.ts new file mode 100644 index 00000000000..ac0bc5ba225 --- /dev/null +++ b/docs/frontend.ts @@ -0,0 +1,206 @@ +// ---------------------------------------------------------------------------- +// Paint module: + +interface Clear { + kind: "clear"; + fill_style: string; +} + +interface Line { + kind: "line"; + from: [number, number]; + line_width: number; + stroke_style: string; + to: [number, number]; +} + +interface Circle { + kind: "circle"; + center: [number, number]; + fill_style: string; + radius: number; +} + +interface RoundedRect { + kind: "rounded_rect"; + fill_style: string; + pos: [number, number]; + radius: number; + size: [number, number]; +} + +interface Text { + kind: "text"; + fill_style: string; + font: string; + pos: [number, number]; + text: string; + text_align: "start" | "center" | "end"; +} + +type PaintCmd = Clear | Line | Circle | RoundedRect | Text; + +function paintCommand(canvas, cmd: PaintCmd) { + const ctx = canvas.getContext("2d"); + + switch (cmd.kind) { + case "clear": + ctx.fillStyle = cmd.fill_style; + ctx.clearRect(0, 0, canvas.width, canvas.height); + return; + + case "line": + ctx.beginPath(); + ctx.lineWidth = cmd.line_width; + ctx.strokeStyle = cmd.stroke_style; + ctx.moveTo(cmd.from[0], cmd.from[1]); + ctx.lineTo(cmd.to[0], cmd.to[1]); + ctx.stroke(); + return; + + case "circle": + ctx.fillStyle = cmd.fill_style; + ctx.beginPath(); + ctx.arc(cmd.center[0], cmd.center[1], cmd.radius, 0, 2 * Math.PI, false); + ctx.fill(); + return; + + case "rounded_rect": + ctx.fillStyle = cmd.fill_style; + const x = cmd.pos[0]; + const y = cmd.pos[1]; + const width = cmd.size[0]; + const height = cmd.size[1]; + const radius = cmd.radius; + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo( + x + width, + y + height, + x + width - radius, + y + height, + ); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + ctx.fill(); + return; + + case "text": + ctx.font = cmd.font; + ctx.fillStyle = cmd.fill_style; + ctx.textAlign = cmd.text_align; + ctx.fillText(cmd.text, cmd.pos[0], cmd.pos[1]); + return; + } +} + +// ---------------------------------------------------------------------------- + +interface Coord { + x: number; + y: number; +} + +interface Input { + mouse_x: number; + mouse_y: number; + screen_height: number; + screen_width: number; + // TODO: mouse down etc +} + +// ---------------------------------------------------------------------------- + +// the `wasm_bindgen` global is set to the exports of the Rust module. Override with wasm-bindgen --no-modules-global +declare var wasm_bindgen: any; + +// we'll defer our execution until the wasm is ready to go +function wasm_loaded() { + console.log(`wasm loaded`); + initialize(); +} + +// here we tell bindgen the path to the wasm file so it can start +// initialization and return to us a promise when it's done +wasm_bindgen("./emgui_bg.wasm") + .then(wasm_loaded) + .catch(console.error); + +function rust_gui(input: Input): PaintCmd[] { + return JSON.parse(wasm_bindgen.show_gui(JSON.stringify(input))); +} + +// ---------------------------------------------------------------------------- + +function js_gui(input: Input): PaintCmd[] { + const commands = []; + + commands.push({ + fillStyle: "#111111", + kind: "clear", + }); + + commands.push({ + fillStyle: "#ff1111", + kind: "rounded_rect", + pos: [100, 100], + radius: 20, + size: [200, 100], + }); + + return commands; +} + +function paint_gui(canvas, mouse_pos) { + const input = { + mouse_x: mouse_pos.x, + mouse_y: mouse_pos.y, + screen_height: canvas.height, + screen_width: canvas.width, + }; + const commands = rust_gui(input); + + for (const cmd of commands) { + paintCommand(canvas, cmd); + } +} + +// ---------------------------------------------------------------------------- + +function mouse_pos_from_event(canvas, evt): Coord { + const rect = canvas.getBoundingClientRect(); + return { + x: evt.clientX - rect.left, + y: evt.clientY - rect.top, + }; +} + +function initialize() { + const canvas = document.getElementById("canvas"); + + canvas.addEventListener( + "mousemove", + (evt) => { + const mouse_pos = mouse_pos_from_event(canvas, evt); + paint_gui(canvas, mouse_pos); + }, + false, + ); + + canvas.addEventListener( + "mousedown", + (evt) => { + const mouse_pos = mouse_pos_from_event(canvas, evt); + paint_gui(canvas, mouse_pos); + }, + false, + ); + + paint_gui(canvas, { x: 0, y: 0 }); +} diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000000..89e65903775 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,44 @@ + + + + + Gui Experiment + + + + + + + + + + + + + + + + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000000..0f4b0e1f270 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,70 @@ +extern crate serde; +extern crate serde_json; +extern crate wasm_bindgen; +extern crate web_sys; +#[macro_use] +extern crate serde_derive; + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +#[derive(Deserialize)] +pub struct Input { + pub screen_width: f32, + pub screen_height: f32, + pub mouse_x: f32, + pub mouse_y: f32, +} + +#[derive(Serialize)] +#[serde(rename_all = "snake_case")] +enum TextAlign { + Start, + Center, + End, +} + +#[derive(Serialize)] +#[serde(rename_all = "snake_case", tag = "kind")] +enum PaintCmd { + Clear { + fill_style: String, + }, + RoundedRect { + fill_style: String, + pos: [f32; 2], + size: [f32; 2], + radius: f32, + }, + Text { + fill_style: String, + font: String, + pos: [f32; 2], + text: String, + text_align: TextAlign, + }, +} + +#[wasm_bindgen] +pub fn show_gui(input_json: &str) -> String { + let input: Input = serde_json::from_str(input_json).unwrap(); + let commands = [ + PaintCmd::Clear { + fill_style: "#44444400".to_string(), + }, + PaintCmd::RoundedRect { + fill_style: "#1111ff".to_string(), + pos: [100.0, 100.0], + radius: 40.0, + size: [200.0, 200.0], + }, + PaintCmd::Text { + fill_style: "#11ff00".to_string(), + font: "14px Palatino".to_string(), + pos: [200.0, 32.0], + text: format!("Mouse pos: {} {}", input.mouse_x, input.mouse_y), + text_align: TextAlign::Center, + }, + ]; + serde_json::to_string(&commands).unwrap() +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000000..1683d98ec77 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "es2015", + }, + "include": [ + "docs/**/*" + ] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 00000000000..6983dcaf15a --- /dev/null +++ b/tslint.json @@ -0,0 +1,15 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "interface-name": [true, "never-prefix"], + "max-classes-per-file": [false], + "no-bitwise": false, + "no-console": false, + "variable-name": false + }, + "rulesDirectory": [] +}