Browse Source

Initial commit: Closed loop of TS->Rust->TS

pull/1/head
Emil Ernerfeldt 6 years ago
commit
856bbf4dae
  1. 2
      .gitignore
  2. 22
      Cargo.toml
  3. 21
      README.md
  4. 33
      build.sh
  5. 6
      build_and_run.sh
  6. 11
      docs/emgui.d.ts
  7. 151
      docs/emgui.js
  8. BIN
      docs/emgui_bg.wasm
  9. 112
      docs/frontend.js
  10. 206
      docs/frontend.ts
  11. 44
      docs/index.html
  12. 70
      src/lib.rs
  13. 8
      tsconfig.json
  14. 15
      tslint.json

2
.gitignore

@ -0,0 +1,2 @@
*.sublime*
/target

22
Cargo.toml

@ -0,0 +1,22 @@
[package]
name = "emgui"
version = "0.1.0"
authors = ["Emil Ernerfeldt <emilernerfeldt@gmail.com>"]
[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"

21
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.

33
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

6
build_and_run.sh

@ -0,0 +1,6 @@
#!/bin/bash
set -eu
./build.sh
open "docs/index.html"

11
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
}

151
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);
})();

BIN
docs/emgui_bg.wasm

Binary file not shown.

112
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 });
}

206
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 });
}

44
docs/index.html

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<head>
<title>Gui Experiment</title>
<style>
html {
/* Remove touch delay: */
touch-action: manipulation;
}
body {
background: #111111;
color: #bbbbbb;
max-width: 480px;
margin: auto;
}
</style>
</head>
<body>
<script>
// The `--no-modules`-generated JS from `wasm-bindgen` attempts to use
// `WebAssembly.instantiateStreaming` to instantiate the wasm module,
// but this doesn't work with `file://` urls. This example is frequently
// viewed by simply opening `index.html` in a browser (with a `file://`
// url), so it would fail if we were to call this function!
//
// Work around this for now by deleting the function to ensure that the
// `no_modules.js` script doesn't have access to it. You won't need this
// hack when deploying over HTTP.
delete WebAssembly.instantiateStreaming;
</script>
<!-- this is the JS generated by the `wasm-bindgen` CLI tool -->
<script src="emgui.js"></script>
<script src="frontend.js" type="module"></script>
<!-- TODO: make this cover the entire screen, with resize and all -->
<canvas id="canvas" width="480" height="800"></canvas>
</body>
</html>

70
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()
}

8
tsconfig.json

@ -0,0 +1,8 @@
{
"compilerOptions": {
"module": "es2015",
},
"include": [
"docs/**/*"
]
}

15
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": []
}
Loading…
Cancel
Save