Skip to content

Commit

Permalink
Initial commit: Closed loop of TS->Rust->TS
Browse files Browse the repository at this point in the history
emilk committed Dec 23, 2018

Unverified

This user has not yet uploaded their public signing key.
0 parents commit 856bbf4
Showing 14 changed files with 701 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.sublime*
/target
22 changes: 22 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 33 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -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 changes: 6 additions & 0 deletions build_and_run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
set -eu

./build.sh

open "docs/index.html"
11 changes: 11 additions & 0 deletions docs/emgui.d.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 151 additions & 0 deletions docs/emgui.js
Original file line number Diff line number Diff line change
@@ -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);
})();
Binary file added docs/emgui_bg.wasm
Binary file not shown.
112 changes: 112 additions & 0 deletions docs/frontend.js
Original file line number Diff line number Diff line change
@@ -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 changes: 206 additions & 0 deletions docs/frontend.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 44 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
@@ -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 changes: 70 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 8 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"module": "es2015",
},
"include": [
"docs/**/*"
]
}
15 changes: 15 additions & 0 deletions tslint.json
Original file line number Diff line number Diff line change
@@ -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": []
}

0 comments on commit 856bbf4

Please sign in to comment.