├── .github └── dependabot.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── internal └── example │ ├── example.gif │ ├── example.wasm │ ├── index.html │ ├── main.go │ └── wasm_exec.js └── renderer ├── keymapping.go ├── render.go ├── renderer.go └── txcache.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: daily -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ebitengine rendering backend for the [pure Go imgui port](https://github.com/Splizard/imgui) 2 | 3 | This uses a [fork](https://github.com/zeozeozeo/imgui) of the version by [Splizard](https://github.com/Splizard), which adds some minor changes to it. 4 | 5 | [![Example](https://github.com/zeozeozeo/ebitengine-imgui-go/blob/main/internal/example/example.gif?raw=true)](https://zeozeozeo.github.io/ebitengine-imgui-go/internal/example) 6 | 7 | # [Live demo](https://zeozeozeo.github.io/ebitengine-imgui-go/internal/example) 8 | 9 | # [Example](https://github.com/zeozeozeo/ebitengine-imgui-go/blob/main/internal/example/main.go) 10 | 11 | # Benefits of not using CGo 12 | 13 | - Cross-compilation 14 | - WebAssembly 15 | - Pure Go :D 16 | 17 | # Building the example for WebAssembly 18 | 19 | 1. Clone the repository 20 | 2. Navigate into the example directory: `cd internal/example` (on Linux) 21 | 3. Build the example: 22 | 23 | On Linux: 24 | 25 | ``` 26 | env GOOS=js GOARCH=wasm go build -o example.wasm main.go 27 | ``` 28 | 29 | On Windows Powershell: 30 | 31 | ``` 32 | $Env:GOOS = 'js' 33 | $Env:GOARCH = 'wasm' 34 | go build -o yourgame.wasm github.com/yourname/yourgame 35 | Remove-Item Env:GOOS 36 | Remove-Item Env:GOARCH 37 | ``` 38 | 39 | 4. Copy `wasm_exec.js` into the current directory: 40 | 41 | On Linux: 42 | 43 | ``` 44 | cp $(go env GOROOT)/misc/wasm/wasm_exec.js . 45 | ``` 46 | 47 | On Windows Powershell: 48 | 49 | ``` 50 | $goroot = go env GOROOT 51 | cp $goroot\misc\wasm\wasm_exec.js . 52 | ``` 53 | 54 | 5. Create this HTML file 55 | 56 | ```html 57 | 58 | 59 | 76 | ``` 77 | 78 | 6. Start a local HTTP server and open the page in your browser 79 | 80 | If you want to embed the game into another page, use iframes (assuming that `main.html` is the name of the above HTML file): 81 | 82 | ``` 83 | 84 | 85 | ``` 86 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zeozeozeo/ebitengine-imgui-go 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.8.5 7 | github.com/zeozeozeo/imgui v0.0.0-20221130185656-4f2288df11b4 8 | ) 9 | 10 | require ( 11 | github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 // indirect 12 | github.com/ebitengine/hideconsole v1.0.0 // indirect 13 | github.com/ebitengine/purego v0.8.0 // indirect 14 | github.com/jezek/xgb v1.1.1 // indirect 15 | golang.org/x/sync v0.8.0 // indirect 16 | golang.org/x/sys v0.25.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 h1:Gk1XUEttOk0/hb6Tq3WkmutWa0ZLhNn/6fc6XZpM7tM= 2 | github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325/go.mod h1:ulhSQcbPioQrallSuIzF8l1NKQoD7xmMZc5NxzibUMY= 3 | github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= 4 | github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= 5 | github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE= 6 | github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 7 | github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM= 8 | github.com/go-gl/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 9 | github.com/hajimehoshi/ebiten/v2 v2.8.5 h1:w1/3XxjEwIo+amtQCOnCrwGzu4e6dr0ewu83JUKoxrM= 10 | github.com/hajimehoshi/ebiten/v2 v2.8.5/go.mod h1:SXx/whkvpfsavGo6lvZykprerakl+8Uo1X8d2U5aAnA= 11 | github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= 12 | github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= 13 | github.com/zeozeozeo/imgui v0.0.0-20221130185656-4f2288df11b4 h1:whJxzk6exYpUN+OTeYEvZDzZpc7sKaNYCH+cjxvmMhM= 14 | github.com/zeozeozeo/imgui v0.0.0-20221130185656-4f2288df11b4/go.mod h1:RWg4ESob7wVmqiIhkm9QrKBBp/bw6zwTxO4OitAtHsY= 15 | golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= 16 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 17 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 18 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 19 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 20 | -------------------------------------------------------------------------------- /internal/example/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeozeozeo/ebitengine-imgui-go/deef603245697bacb37b32e871542a5fb36b0e94/internal/example/example.gif -------------------------------------------------------------------------------- /internal/example/example.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeozeozeo/ebitengine-imgui-go/deef603245697bacb37b32e871542a5fb36b0e94/internal/example/example.wasm -------------------------------------------------------------------------------- /internal/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ImGui on WebAssembly 8 | 15 | 16 | 17 | 18 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /internal/example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hajimehoshi/ebiten/v2" 7 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 8 | "github.com/hajimehoshi/ebiten/v2/inpututil" 9 | "github.com/zeozeozeo/ebitengine-imgui-go/renderer" 10 | "github.com/zeozeozeo/imgui" 11 | ) 12 | 13 | func main() { 14 | // create a new imgui context, you can provide your font atlas if you want, 15 | // but we'll just pass nil for the default one 16 | mgr := renderer.New(nil) 17 | mgr.Ctx.IO.IniFilename = "" // disable imgui.ini 18 | 19 | ebiten.SetWindowSize(1280, 720) 20 | ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) 21 | ebiten.SetTPS(ebiten.SyncWithFPS) 22 | 23 | gg := &Game{ 24 | mgr: mgr, 25 | deviceScale: ebiten.DeviceScaleFactor(), 26 | demoWindowOpen: true, 27 | metricsWindowOpen: true, 28 | } 29 | 30 | ebiten.RunGame(gg) 31 | } 32 | 33 | type Game struct { 34 | mgr *renderer.Manager 35 | 36 | deviceScale float64 37 | retina bool 38 | w, h int 39 | demoWindowOpen bool 40 | metricsWindowOpen bool 41 | consoleOutput []string 42 | } 43 | 44 | func (game *Game) Draw(screen *ebiten.Image) { 45 | ebitenutil.DebugPrintAt( 46 | screen, 47 | fmt.Sprintf( 48 | "tps: %.2f\nfps: %.2f\n(c)lipmask: %t", 49 | ebiten.ActualTPS(), 50 | ebiten.ActualFPS(), 51 | game.mgr.ClipMask, 52 | ), 53 | 10, 54 | 2, 55 | ) 56 | 57 | game.mgr.Draw(screen) 58 | } 59 | 60 | func (game *Game) Update() error { 61 | // update imgui state 62 | game.mgr.Update(1.0 / float32(ebiten.ActualTPS())) 63 | 64 | // disable or enable the clipmask when C is pressed 65 | // usually you'd always have this enabled 66 | if inpututil.IsKeyJustPressed(ebiten.KeyC) { 67 | game.mgr.ClipMask = !game.mgr.ClipMask 68 | } 69 | 70 | game.mgr.BeginFrame() // start a new imgui frame 71 | 72 | // test window 73 | imgui.Begin("Test window", nil, 0) // 0 = default window flags 74 | { 75 | imgui.Text("Hello, world!") 76 | if imgui.Button("button") { 77 | fmt.Println("button pressed") 78 | game.consoleOutput = append(game.consoleOutput, "button pressed") 79 | } 80 | 81 | // draw a second button on the same line 82 | imgui.SameLine(0, 4) 83 | if imgui.Button("button 2") { 84 | fmt.Println("button 2 pressed") 85 | game.consoleOutput = append(game.consoleOutput, "button 2 pressed") 86 | } 87 | } 88 | imgui.End() 89 | 90 | // options window 91 | imgui.SetNextWindowPos(imgui.NewImVec2(300, 300), imgui.ImGuiCond_FirstUseEver, *imgui.NewImVec2(0, 0)) 92 | imgui.Begin("Options", nil, 0) 93 | { 94 | imgui.Checkbox("Retina", &game.retina) 95 | imgui.Checkbox("Demo window", &game.demoWindowOpen) 96 | imgui.Checkbox("Metrics window", &game.metricsWindowOpen) 97 | } 98 | imgui.End() 99 | 100 | // console window 101 | imgui.SetNextWindowPos(imgui.NewImVec2(50, 200), imgui.ImGuiCond_FirstUseEver, *imgui.NewImVec2(0, 0)) 102 | imgui.SetNextWindowSize(imgui.NewImVec2(200, 300), imgui.ImGuiCond_FirstUseEver) 103 | imgui.Begin("Console", nil, 0) 104 | { 105 | imgui.Text("console output:") 106 | for idx, s := range game.consoleOutput { 107 | imgui.PushID(int32(idx)) 108 | imgui.Text("%s", s) 109 | imgui.PopID() 110 | } 111 | } 112 | imgui.End() 113 | 114 | // demo window (WARNING: this crashes a lot :p) 115 | if game.demoWindowOpen { 116 | imgui.ShowDemoWindow(&game.demoWindowOpen) 117 | } 118 | 119 | if game.metricsWindowOpen { 120 | imgui.ShowMetricsWindow(&game.metricsWindowOpen) 121 | } 122 | 123 | game.mgr.EndFrame() 124 | return nil 125 | } 126 | 127 | func (game *Game) Layout(outsideWidth, outsideHeight int) (int, int) { 128 | if game.retina { 129 | m := ebiten.DeviceScaleFactor() 130 | game.w = int(float64(outsideWidth) * m) 131 | game.h = int(float64(outsideHeight) * m) 132 | } else { 133 | game.w = outsideWidth 134 | game.h = outsideHeight 135 | } 136 | 137 | game.mgr.SetDisplaySize(float32(game.w), float32(game.h)) 138 | return game.w, game.h 139 | } 140 | -------------------------------------------------------------------------------- /internal/example/wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | "use strict"; 6 | 7 | (() => { 8 | const enosys = () => { 9 | const err = new Error("not implemented"); 10 | err.code = "ENOSYS"; 11 | return err; 12 | }; 13 | 14 | if (!globalThis.fs) { 15 | let outputBuf = ""; 16 | globalThis.fs = { 17 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 18 | writeSync(fd, buf) { 19 | outputBuf += decoder.decode(buf); 20 | const nl = outputBuf.lastIndexOf("\n"); 21 | if (nl != -1) { 22 | console.log(outputBuf.substr(0, nl)); 23 | outputBuf = outputBuf.substr(nl + 1); 24 | } 25 | return buf.length; 26 | }, 27 | write(fd, buf, offset, length, position, callback) { 28 | if (offset !== 0 || length !== buf.length || position !== null) { 29 | callback(enosys()); 30 | return; 31 | } 32 | const n = this.writeSync(fd, buf); 33 | callback(null, n); 34 | }, 35 | chmod(path, mode, callback) { callback(enosys()); }, 36 | chown(path, uid, gid, callback) { callback(enosys()); }, 37 | close(fd, callback) { callback(enosys()); }, 38 | fchmod(fd, mode, callback) { callback(enosys()); }, 39 | fchown(fd, uid, gid, callback) { callback(enosys()); }, 40 | fstat(fd, callback) { callback(enosys()); }, 41 | fsync(fd, callback) { callback(null); }, 42 | ftruncate(fd, length, callback) { callback(enosys()); }, 43 | lchown(path, uid, gid, callback) { callback(enosys()); }, 44 | link(path, link, callback) { callback(enosys()); }, 45 | lstat(path, callback) { callback(enosys()); }, 46 | mkdir(path, perm, callback) { callback(enosys()); }, 47 | open(path, flags, mode, callback) { callback(enosys()); }, 48 | read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, 49 | readdir(path, callback) { callback(enosys()); }, 50 | readlink(path, callback) { callback(enosys()); }, 51 | rename(from, to, callback) { callback(enosys()); }, 52 | rmdir(path, callback) { callback(enosys()); }, 53 | stat(path, callback) { callback(enosys()); }, 54 | symlink(path, link, callback) { callback(enosys()); }, 55 | truncate(path, length, callback) { callback(enosys()); }, 56 | unlink(path, callback) { callback(enosys()); }, 57 | utimes(path, atime, mtime, callback) { callback(enosys()); }, 58 | }; 59 | } 60 | 61 | if (!globalThis.process) { 62 | globalThis.process = { 63 | getuid() { return -1; }, 64 | getgid() { return -1; }, 65 | geteuid() { return -1; }, 66 | getegid() { return -1; }, 67 | getgroups() { throw enosys(); }, 68 | pid: -1, 69 | ppid: -1, 70 | umask() { throw enosys(); }, 71 | cwd() { throw enosys(); }, 72 | chdir() { throw enosys(); }, 73 | } 74 | } 75 | 76 | if (!globalThis.crypto) { 77 | throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); 78 | } 79 | 80 | if (!globalThis.performance) { 81 | throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); 82 | } 83 | 84 | if (!globalThis.TextEncoder) { 85 | throw new Error("globalThis.TextEncoder is not available, polyfill required"); 86 | } 87 | 88 | if (!globalThis.TextDecoder) { 89 | throw new Error("globalThis.TextDecoder is not available, polyfill required"); 90 | } 91 | 92 | const encoder = new TextEncoder("utf-8"); 93 | const decoder = new TextDecoder("utf-8"); 94 | 95 | globalThis.Go = class { 96 | constructor() { 97 | this.argv = ["js"]; 98 | this.env = {}; 99 | this.exit = (code) => { 100 | if (code !== 0) { 101 | console.warn("exit code:", code); 102 | } 103 | }; 104 | this._exitPromise = new Promise((resolve) => { 105 | this._resolveExitPromise = resolve; 106 | }); 107 | this._pendingEvent = null; 108 | this._scheduledTimeouts = new Map(); 109 | this._nextCallbackTimeoutID = 1; 110 | 111 | const setInt64 = (addr, v) => { 112 | this.mem.setUint32(addr + 0, v, true); 113 | this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); 114 | } 115 | 116 | const getInt64 = (addr) => { 117 | const low = this.mem.getUint32(addr + 0, true); 118 | const high = this.mem.getInt32(addr + 4, true); 119 | return low + high * 4294967296; 120 | } 121 | 122 | const loadValue = (addr) => { 123 | const f = this.mem.getFloat64(addr, true); 124 | if (f === 0) { 125 | return undefined; 126 | } 127 | if (!isNaN(f)) { 128 | return f; 129 | } 130 | 131 | const id = this.mem.getUint32(addr, true); 132 | return this._values[id]; 133 | } 134 | 135 | const storeValue = (addr, v) => { 136 | const nanHead = 0x7FF80000; 137 | 138 | if (typeof v === "number" && v !== 0) { 139 | if (isNaN(v)) { 140 | this.mem.setUint32(addr + 4, nanHead, true); 141 | this.mem.setUint32(addr, 0, true); 142 | return; 143 | } 144 | this.mem.setFloat64(addr, v, true); 145 | return; 146 | } 147 | 148 | if (v === undefined) { 149 | this.mem.setFloat64(addr, 0, true); 150 | return; 151 | } 152 | 153 | let id = this._ids.get(v); 154 | if (id === undefined) { 155 | id = this._idPool.pop(); 156 | if (id === undefined) { 157 | id = this._values.length; 158 | } 159 | this._values[id] = v; 160 | this._goRefCounts[id] = 0; 161 | this._ids.set(v, id); 162 | } 163 | this._goRefCounts[id]++; 164 | let typeFlag = 0; 165 | switch (typeof v) { 166 | case "object": 167 | if (v !== null) { 168 | typeFlag = 1; 169 | } 170 | break; 171 | case "string": 172 | typeFlag = 2; 173 | break; 174 | case "symbol": 175 | typeFlag = 3; 176 | break; 177 | case "function": 178 | typeFlag = 4; 179 | break; 180 | } 181 | this.mem.setUint32(addr + 4, nanHead | typeFlag, true); 182 | this.mem.setUint32(addr, id, true); 183 | } 184 | 185 | const loadSlice = (addr) => { 186 | const array = getInt64(addr + 0); 187 | const len = getInt64(addr + 8); 188 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 189 | } 190 | 191 | const loadSliceOfValues = (addr) => { 192 | const array = getInt64(addr + 0); 193 | const len = getInt64(addr + 8); 194 | const a = new Array(len); 195 | for (let i = 0; i < len; i++) { 196 | a[i] = loadValue(array + i * 8); 197 | } 198 | return a; 199 | } 200 | 201 | const loadString = (addr) => { 202 | const saddr = getInt64(addr + 0); 203 | const len = getInt64(addr + 8); 204 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 205 | } 206 | 207 | const timeOrigin = Date.now() - performance.now(); 208 | this.importObject = { 209 | go: { 210 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 211 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 212 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 213 | // This changes the SP, thus we have to update the SP used by the imported function. 214 | 215 | // func wasmExit(code int32) 216 | "runtime.wasmExit": (sp) => { 217 | sp >>>= 0; 218 | const code = this.mem.getInt32(sp + 8, true); 219 | this.exited = true; 220 | delete this._inst; 221 | delete this._values; 222 | delete this._goRefCounts; 223 | delete this._ids; 224 | delete this._idPool; 225 | this.exit(code); 226 | }, 227 | 228 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 229 | "runtime.wasmWrite": (sp) => { 230 | sp >>>= 0; 231 | const fd = getInt64(sp + 8); 232 | const p = getInt64(sp + 16); 233 | const n = this.mem.getInt32(sp + 24, true); 234 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 235 | }, 236 | 237 | // func resetMemoryDataView() 238 | "runtime.resetMemoryDataView": (sp) => { 239 | sp >>>= 0; 240 | this.mem = new DataView(this._inst.exports.mem.buffer); 241 | }, 242 | 243 | // func nanotime1() int64 244 | "runtime.nanotime1": (sp) => { 245 | sp >>>= 0; 246 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 247 | }, 248 | 249 | // func walltime() (sec int64, nsec int32) 250 | "runtime.walltime": (sp) => { 251 | sp >>>= 0; 252 | const msec = (new Date).getTime(); 253 | setInt64(sp + 8, msec / 1000); 254 | this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); 255 | }, 256 | 257 | // func scheduleTimeoutEvent(delay int64) int32 258 | "runtime.scheduleTimeoutEvent": (sp) => { 259 | sp >>>= 0; 260 | const id = this._nextCallbackTimeoutID; 261 | this._nextCallbackTimeoutID++; 262 | this._scheduledTimeouts.set(id, setTimeout( 263 | () => { 264 | this._resume(); 265 | while (this._scheduledTimeouts.has(id)) { 266 | // for some reason Go failed to register the timeout event, log and try again 267 | // (temporary workaround for https://github.com/golang/go/issues/28975) 268 | console.warn("scheduleTimeoutEvent: missed timeout event"); 269 | this._resume(); 270 | } 271 | }, 272 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early 273 | )); 274 | this.mem.setInt32(sp + 16, id, true); 275 | }, 276 | 277 | // func clearTimeoutEvent(id int32) 278 | "runtime.clearTimeoutEvent": (sp) => { 279 | sp >>>= 0; 280 | const id = this.mem.getInt32(sp + 8, true); 281 | clearTimeout(this._scheduledTimeouts.get(id)); 282 | this._scheduledTimeouts.delete(id); 283 | }, 284 | 285 | // func getRandomData(r []byte) 286 | "runtime.getRandomData": (sp) => { 287 | sp >>>= 0; 288 | crypto.getRandomValues(loadSlice(sp + 8)); 289 | }, 290 | 291 | // func finalizeRef(v ref) 292 | "syscall/js.finalizeRef": (sp) => { 293 | sp >>>= 0; 294 | const id = this.mem.getUint32(sp + 8, true); 295 | this._goRefCounts[id]--; 296 | if (this._goRefCounts[id] === 0) { 297 | const v = this._values[id]; 298 | this._values[id] = null; 299 | this._ids.delete(v); 300 | this._idPool.push(id); 301 | } 302 | }, 303 | 304 | // func stringVal(value string) ref 305 | "syscall/js.stringVal": (sp) => { 306 | sp >>>= 0; 307 | storeValue(sp + 24, loadString(sp + 8)); 308 | }, 309 | 310 | // func valueGet(v ref, p string) ref 311 | "syscall/js.valueGet": (sp) => { 312 | sp >>>= 0; 313 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 314 | sp = this._inst.exports.getsp() >>> 0; // see comment above 315 | storeValue(sp + 32, result); 316 | }, 317 | 318 | // func valueSet(v ref, p string, x ref) 319 | "syscall/js.valueSet": (sp) => { 320 | sp >>>= 0; 321 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 322 | }, 323 | 324 | // func valueDelete(v ref, p string) 325 | "syscall/js.valueDelete": (sp) => { 326 | sp >>>= 0; 327 | Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); 328 | }, 329 | 330 | // func valueIndex(v ref, i int) ref 331 | "syscall/js.valueIndex": (sp) => { 332 | sp >>>= 0; 333 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 334 | }, 335 | 336 | // valueSetIndex(v ref, i int, x ref) 337 | "syscall/js.valueSetIndex": (sp) => { 338 | sp >>>= 0; 339 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 340 | }, 341 | 342 | // func valueCall(v ref, m string, args []ref) (ref, bool) 343 | "syscall/js.valueCall": (sp) => { 344 | sp >>>= 0; 345 | try { 346 | const v = loadValue(sp + 8); 347 | const m = Reflect.get(v, loadString(sp + 16)); 348 | const args = loadSliceOfValues(sp + 32); 349 | const result = Reflect.apply(m, v, args); 350 | sp = this._inst.exports.getsp() >>> 0; // see comment above 351 | storeValue(sp + 56, result); 352 | this.mem.setUint8(sp + 64, 1); 353 | } catch (err) { 354 | sp = this._inst.exports.getsp() >>> 0; // see comment above 355 | storeValue(sp + 56, err); 356 | this.mem.setUint8(sp + 64, 0); 357 | } 358 | }, 359 | 360 | // func valueInvoke(v ref, args []ref) (ref, bool) 361 | "syscall/js.valueInvoke": (sp) => { 362 | sp >>>= 0; 363 | try { 364 | const v = loadValue(sp + 8); 365 | const args = loadSliceOfValues(sp + 16); 366 | const result = Reflect.apply(v, undefined, args); 367 | sp = this._inst.exports.getsp() >>> 0; // see comment above 368 | storeValue(sp + 40, result); 369 | this.mem.setUint8(sp + 48, 1); 370 | } catch (err) { 371 | sp = this._inst.exports.getsp() >>> 0; // see comment above 372 | storeValue(sp + 40, err); 373 | this.mem.setUint8(sp + 48, 0); 374 | } 375 | }, 376 | 377 | // func valueNew(v ref, args []ref) (ref, bool) 378 | "syscall/js.valueNew": (sp) => { 379 | sp >>>= 0; 380 | try { 381 | const v = loadValue(sp + 8); 382 | const args = loadSliceOfValues(sp + 16); 383 | const result = Reflect.construct(v, args); 384 | sp = this._inst.exports.getsp() >>> 0; // see comment above 385 | storeValue(sp + 40, result); 386 | this.mem.setUint8(sp + 48, 1); 387 | } catch (err) { 388 | sp = this._inst.exports.getsp() >>> 0; // see comment above 389 | storeValue(sp + 40, err); 390 | this.mem.setUint8(sp + 48, 0); 391 | } 392 | }, 393 | 394 | // func valueLength(v ref) int 395 | "syscall/js.valueLength": (sp) => { 396 | sp >>>= 0; 397 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 398 | }, 399 | 400 | // valuePrepareString(v ref) (ref, int) 401 | "syscall/js.valuePrepareString": (sp) => { 402 | sp >>>= 0; 403 | const str = encoder.encode(String(loadValue(sp + 8))); 404 | storeValue(sp + 16, str); 405 | setInt64(sp + 24, str.length); 406 | }, 407 | 408 | // valueLoadString(v ref, b []byte) 409 | "syscall/js.valueLoadString": (sp) => { 410 | sp >>>= 0; 411 | const str = loadValue(sp + 8); 412 | loadSlice(sp + 16).set(str); 413 | }, 414 | 415 | // func valueInstanceOf(v ref, t ref) bool 416 | "syscall/js.valueInstanceOf": (sp) => { 417 | sp >>>= 0; 418 | this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); 419 | }, 420 | 421 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 422 | "syscall/js.copyBytesToGo": (sp) => { 423 | sp >>>= 0; 424 | const dst = loadSlice(sp + 8); 425 | const src = loadValue(sp + 32); 426 | if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { 427 | this.mem.setUint8(sp + 48, 0); 428 | return; 429 | } 430 | const toCopy = src.subarray(0, dst.length); 431 | dst.set(toCopy); 432 | setInt64(sp + 40, toCopy.length); 433 | this.mem.setUint8(sp + 48, 1); 434 | }, 435 | 436 | // func copyBytesToJS(dst ref, src []byte) (int, bool) 437 | "syscall/js.copyBytesToJS": (sp) => { 438 | sp >>>= 0; 439 | const dst = loadValue(sp + 8); 440 | const src = loadSlice(sp + 16); 441 | if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { 442 | this.mem.setUint8(sp + 48, 0); 443 | return; 444 | } 445 | const toCopy = src.subarray(0, dst.length); 446 | dst.set(toCopy); 447 | setInt64(sp + 40, toCopy.length); 448 | this.mem.setUint8(sp + 48, 1); 449 | }, 450 | 451 | "debug": (value) => { 452 | console.log(value); 453 | }, 454 | } 455 | }; 456 | } 457 | 458 | async run(instance) { 459 | if (!(instance instanceof WebAssembly.Instance)) { 460 | throw new Error("Go.run: WebAssembly.Instance expected"); 461 | } 462 | this._inst = instance; 463 | this.mem = new DataView(this._inst.exports.mem.buffer); 464 | this._values = [ // JS values that Go currently has references to, indexed by reference id 465 | NaN, 466 | 0, 467 | null, 468 | true, 469 | false, 470 | globalThis, 471 | this, 472 | ]; 473 | this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id 474 | this._ids = new Map([ // mapping from JS values to reference ids 475 | [0, 1], 476 | [null, 2], 477 | [true, 3], 478 | [false, 4], 479 | [globalThis, 5], 480 | [this, 6], 481 | ]); 482 | this._idPool = []; // unused ids that have been garbage collected 483 | this.exited = false; // whether the Go program has exited 484 | 485 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 486 | let offset = 4096; 487 | 488 | const strPtr = (str) => { 489 | const ptr = offset; 490 | const bytes = encoder.encode(str + "\0"); 491 | new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); 492 | offset += bytes.length; 493 | if (offset % 8 !== 0) { 494 | offset += 8 - (offset % 8); 495 | } 496 | return ptr; 497 | }; 498 | 499 | const argc = this.argv.length; 500 | 501 | const argvPtrs = []; 502 | this.argv.forEach((arg) => { 503 | argvPtrs.push(strPtr(arg)); 504 | }); 505 | argvPtrs.push(0); 506 | 507 | const keys = Object.keys(this.env).sort(); 508 | keys.forEach((key) => { 509 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 510 | }); 511 | argvPtrs.push(0); 512 | 513 | const argv = offset; 514 | argvPtrs.forEach((ptr) => { 515 | this.mem.setUint32(offset, ptr, true); 516 | this.mem.setUint32(offset + 4, 0, true); 517 | offset += 8; 518 | }); 519 | 520 | // The linker guarantees global data starts from at least wasmMinDataAddr. 521 | // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. 522 | const wasmMinDataAddr = 4096 + 8192; 523 | if (offset >= wasmMinDataAddr) { 524 | throw new Error("total length of command line and environment variables exceeds limit"); 525 | } 526 | 527 | this._inst.exports.run(argc, argv); 528 | if (this.exited) { 529 | this._resolveExitPromise(); 530 | } 531 | await this._exitPromise; 532 | } 533 | 534 | _resume() { 535 | if (this.exited) { 536 | throw new Error("Go program has already exited"); 537 | } 538 | this._inst.exports.resume(); 539 | if (this.exited) { 540 | this._resolveExitPromise(); 541 | } 542 | } 543 | 544 | _makeFuncWrapper(id) { 545 | const go = this; 546 | return function () { 547 | const event = { id: id, this: this, args: arguments }; 548 | go._pendingEvent = event; 549 | go._resume(); 550 | return event.result; 551 | }; 552 | } 553 | } 554 | })(); 555 | -------------------------------------------------------------------------------- /renderer/keymapping.go: -------------------------------------------------------------------------------- 1 | package renderer 2 | 3 | import ( 4 | "github.com/hajimehoshi/ebiten/v2" 5 | "github.com/hajimehoshi/ebiten/v2/inpututil" 6 | "github.com/zeozeozeo/imgui" 7 | ) 8 | 9 | var keys = map[imgui.ImGuiKey]int{ 10 | imgui.ImGuiKey_Tab: int(ebiten.KeyTab), 11 | imgui.ImGuiKey_LeftArrow: int(ebiten.KeyLeft), 12 | imgui.ImGuiKey_RightArrow: int(ebiten.KeyRight), 13 | imgui.ImGuiKey_UpArrow: int(ebiten.KeyUp), 14 | imgui.ImGuiKey_DownArrow: int(ebiten.KeyDown), 15 | imgui.ImGuiKey_PageUp: int(ebiten.KeyPageUp), 16 | imgui.ImGuiKey_PageDown: int(ebiten.KeyPageDown), 17 | imgui.ImGuiKey_Home: int(ebiten.KeyHome), 18 | imgui.ImGuiKey_End: int(ebiten.KeyEnd), 19 | imgui.ImGuiKey_Insert: int(ebiten.KeyInsert), 20 | imgui.ImGuiKey_Delete: int(ebiten.KeyDelete), 21 | imgui.ImGuiKey_Backspace: int(ebiten.KeyBackspace), 22 | imgui.ImGuiKey_Space: int(ebiten.KeySpace), 23 | imgui.ImGuiKey_Enter: int(ebiten.KeyEnter), 24 | imgui.ImGuiKey_Escape: int(ebiten.KeyEscape), 25 | imgui.ImGuiKey_A: int(ebiten.KeyA), 26 | imgui.ImGuiKey_C: int(ebiten.KeyC), 27 | imgui.ImGuiKey_V: int(ebiten.KeyV), 28 | imgui.ImGuiKey_X: int(ebiten.KeyX), 29 | imgui.ImGuiKey_Y: int(ebiten.KeyY), 30 | imgui.ImGuiKey_Z: int(ebiten.KeyZ), 31 | } 32 | 33 | func sendInput(io *imgui.ImGuiIO, inputChars []rune) []rune { 34 | io.KeyAlt = ebiten.IsKeyPressed(ebiten.KeyAlt) 35 | io.KeyShift = ebiten.IsKeyPressed(ebiten.KeyShift) 36 | io.KeyCtrl = ebiten.IsKeyPressed(ebiten.KeyControl) 37 | // TODO: KeySuper 38 | 39 | inputChars = ebiten.AppendInputChars(inputChars) 40 | if len(inputChars) > 0 { 41 | io.AddInputCharacters(string(inputChars)) 42 | inputChars = inputChars[:0] 43 | } 44 | for _, key := range keys { 45 | if inpututil.IsKeyJustPressed(ebiten.Key(key)) { 46 | io.KeysDown[key] = true 47 | } 48 | if inpututil.IsKeyJustReleased(ebiten.Key(key)) { 49 | io.KeysDown[key] = false 50 | } 51 | } 52 | return inputChars 53 | } 54 | 55 | func (m *Manager) setKeyMapping() { 56 | // Keyboard mapping. ImGui will use those indices to peek into the io.KeysDown[] array. 57 | ctx := imgui.GetCurrentContext() 58 | for imguiKey, nativeKey := range keys { 59 | ctx.IO.KeyMap[imguiKey] = int32(nativeKey) 60 | } 61 | } -------------------------------------------------------------------------------- /renderer/render.go: -------------------------------------------------------------------------------- 1 | package renderer 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/hajimehoshi/ebiten/v2" 7 | "github.com/zeozeozeo/imgui" 8 | ) 9 | 10 | func getTexture(width, height int32, pixels []uint32) *ebiten.Image { 11 | n := width * height 12 | pix := make([]uint8, n*4) 13 | 14 | // NOTE: ebiten expects premultiplied-alpha pixels, but imgui 15 | // exports bitmaps in straight-alpha. 16 | for i := int32(0); i < n; i++ { 17 | r := pixels[i] & 0xFF 18 | g := pixels[i] >> 8 & 0xFF 19 | b := pixels[i] >> 16 & 0xFF 20 | a := pixels[i] >> 24 & 0xFF 21 | 22 | alpha := uint16(a) 23 | pix[4*i] = uint8((uint16(r)*alpha + 127) / 255) 24 | pix[4*i+1] = uint8((uint16(g)*alpha + 127) / 255) 25 | pix[4*i+2] = uint8((uint16(b)*alpha + 127) / 255) 26 | pix[4*i+3] = uint8(alpha) 27 | } 28 | 29 | img := ebiten.NewImage(int(width), int(height)) 30 | img.WritePixels(pix) 31 | return img 32 | } 33 | 34 | func convertVertices(verts []imgui.ImDrawVert) []ebiten.Vertex { 35 | vertices := make([]ebiten.Vertex, len(verts)) 36 | 37 | for i := 0; i < len(verts); i++ { 38 | // ImGui uses 32 bit unsigned integers to store colors, 39 | // but Ebiten uses 0-1 floating point numbers 40 | vertices[i] = ebiten.Vertex{ 41 | SrcX: verts[i].Uv.X(), 42 | SrcY: verts[i].Uv.Y(), 43 | DstX: verts[i].Pos.X(), 44 | DstY: verts[i].Pos.Y(), 45 | ColorR: float32(verts[i].Col&0xFF) / 255, 46 | ColorG: float32(verts[i].Col>>8&0xFF) / 255, 47 | ColorB: float32(verts[i].Col>>16&0xFF) / 255, 48 | ColorA: float32(verts[i].Col>>24&0xFF) / 255, 49 | } 50 | } 51 | return vertices 52 | } 53 | 54 | func vcopy(v []ebiten.Vertex) []ebiten.Vertex { 55 | cl := make([]ebiten.Vertex, len(v)) 56 | copy(cl, v) 57 | return cl 58 | } 59 | 60 | // Render the ImGui drawData into the target *ebiten.Image 61 | func Render(target *ebiten.Image, drawData *imgui.ImDrawData, txcache TextureCache, dfilter ebiten.Filter) { 62 | render(target, nil, drawData, txcache, dfilter) 63 | } 64 | 65 | // RenderMasked renders the ImGui drawData into the target *ebiten.Image with ebiten.CompositeModeCopy for masking 66 | func RenderMasked(target *ebiten.Image, mask *ebiten.Image, drawData *imgui.ImDrawData, txcache TextureCache, dfilter ebiten.Filter) { 67 | render(target, mask, drawData, txcache, dfilter) 68 | } 69 | 70 | func lerp(a, b int, t float32) float32 { 71 | return float32(a)*(1-t) + float32(b)*t 72 | } 73 | 74 | func vmultiply(v, vbuf []ebiten.Vertex, bmin, bmax image.Point) { 75 | for i := range vbuf { 76 | vbuf[i].SrcX = lerp(bmin.X, bmax.X, v[i].SrcX) 77 | vbuf[i].SrcY = lerp(bmin.Y, bmax.Y, v[i].SrcY) 78 | } 79 | } 80 | 81 | func indicesToUint16(input []imgui.ImDrawIdx) []uint16 { 82 | out := make([]uint16, len(input)) 83 | for i, val := range input { 84 | out[i] = uint16(val) 85 | } 86 | return out 87 | } 88 | 89 | func render(target *ebiten.Image, mask *ebiten.Image, drawData *imgui.ImDrawData, txcache TextureCache, dfilter ebiten.Filter) { 90 | if !drawData.Valid { 91 | return 92 | } 93 | targetw, targeth := target.Size() 94 | 95 | // image and triangle options 96 | opt := &ebiten.DrawTrianglesOptions{ 97 | Filter: dfilter, 98 | } 99 | 100 | var opt2 *ebiten.DrawImageOptions 101 | if mask != nil { 102 | opt2 = &ebiten.DrawImageOptions{ 103 | CompositeMode: ebiten.CompositeModeSourceOver, 104 | } 105 | } 106 | 107 | for _, clist := range drawData.CmdLists { 108 | // var indexBufferOffset uint32 109 | vertices := convertVertices(clist.VtxBuffer) 110 | verticesMul := vcopy(vertices) 111 | 112 | // draw command buffer 113 | for _, cmd := range clist.CmdBuffer { 114 | if cmd.UserCallback != nil { 115 | cmd.UserCallback(clist, &cmd) 116 | continue 117 | } 118 | // has no user callbacks 119 | 120 | clipRect := cmd.ClipRect 121 | texture := txcache.GetTexture(cmd.GetTexID()) 122 | vmultiply(vertices, verticesMul, texture.Bounds().Min, texture.Bounds().Max) 123 | 124 | indices := indicesToUint16(clist.IdxBuffer[cmd.IdxOffset : cmd.IdxOffset+cmd.ElemCount]) 125 | 126 | // if has no mask, draw triangles like normal 127 | if mask == nil || (clipRect.X() == 0 && clipRect.Y() == 0 && clipRect.Z() == float32(targetw) && clipRect.W() == float32(targeth)) { 128 | target.DrawTriangles(verticesMul, indices, texture, opt) 129 | continue 130 | } 131 | 132 | // has a clip mask 133 | mask.Clear() 134 | opt2.GeoM.Reset() 135 | opt2.GeoM.Translate(float64(clipRect.X()), float64(clipRect.Y())) 136 | 137 | mask.DrawTriangles(verticesMul, indices, texture, opt) 138 | 139 | target.DrawImage(mask.SubImage(image.Rectangle{ 140 | Min: image.Pt(int(clipRect.X()), int(clipRect.Y())), 141 | Max: image.Pt(int(clipRect.Z()), int(clipRect.W())), 142 | }).(*ebiten.Image), opt2) 143 | } 144 | } 145 | 146 | } 147 | 148 | /* 149 | func render(target *ebiten.Image, mask *ebiten.Image, drawData *imgui.ImDrawData, txcache TextureCache, dfilter ebiten.Filter) { 150 | targetw, targeth := target.Size() 151 | if !drawData.Valid { 152 | return 153 | } 154 | 155 | opt := &ebiten.DrawTrianglesOptions{ 156 | Filter: dfilter, 157 | } 158 | var opt2 *ebiten.DrawImageOptions 159 | if mask != nil { 160 | opt2 = &ebiten.DrawImageOptions{ 161 | CompositeMode: ebiten.CompositeModeSourceOver, 162 | } 163 | } 164 | 165 | for _, clist := range drawData.CmdLists { 166 | var indexBufferOffset uint32 167 | indexBuffer := clist.IdxBuffer 168 | 169 | vertices := getVertices(clist.VtxBuffer) 170 | vbuf := vcopy(vertices) 171 | 172 | for _, cmd := range clist.CmdBuffer { 173 | ecount := cmd.ElemCount 174 | 175 | if cmd.UserCallback != nil { 176 | cmd.UserCallback(clist, &cmd) 177 | } else { 178 | clipRect := cmd.ClipRect 179 | texid := cmd.GetTexID() // imgui.ImTextureID 180 | tx := txcache.GetTexture(texid) 181 | vmultiply(vertices, vbuf, tx.Bounds().Min, tx.Bounds().Max) 182 | 183 | if mask == nil || (clipRect.X() == 0 && clipRect.Y() == 0 && clipRect.Z() == float32(targetw) && clipRect.W() == float32(targeth)) { 184 | target.DrawTriangles( 185 | vbuf, 186 | indicesToUint16(indexBuffer[indexBufferOffset:indexBufferOffset+ecount]), 187 | tx, 188 | opt, 189 | ) 190 | } else { 191 | mask.Clear() 192 | opt2.GeoM.Reset() 193 | opt2.GeoM.Translate(float64(clipRect.X()), float64(clipRect.Y())) 194 | 195 | mask.DrawTriangles( 196 | vbuf, 197 | indicesToUint16(indexBuffer[indexBufferOffset:indexBufferOffset+ecount]), 198 | tx, 199 | opt, 200 | ) 201 | 202 | target.DrawImage(mask.SubImage(image.Rectangle{ 203 | Min: image.Pt(int(clipRect.X()), int(clipRect.Y())), 204 | Max: image.Pt(int(clipRect.Z()), int(clipRect.W())), 205 | }).(*ebiten.Image), opt2) 206 | } 207 | } 208 | indexBufferOffset += ecount 209 | } 210 | } 211 | } 212 | */ 213 | -------------------------------------------------------------------------------- /renderer/renderer.go: -------------------------------------------------------------------------------- 1 | package renderer 2 | 3 | import ( 4 | "github.com/hajimehoshi/ebiten/v2" 5 | "github.com/zeozeozeo/imgui" 6 | ) 7 | 8 | type GetCursorFunc func() (x, y float32) 9 | 10 | type Manager struct { 11 | Filter ebiten.Filter 12 | Cache TextureCache 13 | Ctx *imgui.ImGuiContext // ImGui context 14 | cliptxt string 15 | GetCursor GetCursorFunc 16 | SyncInputsFn func() 17 | SyncCursor bool 18 | SyncInputs bool 19 | ControlCursorShape bool 20 | lmask *ebiten.Image 21 | ClipMask bool 22 | 23 | width float32 24 | height float32 25 | screenWidth int 26 | screenHeight int 27 | 28 | inputChars []rune 29 | } 30 | 31 | // Text implements ImGui clipboard 32 | func (m *Manager) Text() (string, error) { 33 | return m.cliptxt, nil 34 | } 35 | 36 | // SetText implements ImGui clipboard 37 | func (m *Manager) SetText(text string) { 38 | m.cliptxt = text 39 | } 40 | 41 | // SetDisplaySize sets the display dimensions. 42 | func (m *Manager) SetDisplaySize(width, height float32) { 43 | m.width = width 44 | m.height = height 45 | } 46 | 47 | // BeginFrame begins a new ImGui frame. 48 | func (m *Manager) BeginFrame() { 49 | imgui.NewFrame() 50 | } 51 | 52 | // EndFrame ends the current ImGui frame. 53 | func (m *Manager) EndFrame() { 54 | imgui.EndFrame() 55 | } 56 | 57 | // New creates a new Manager with a provided font atlas. 58 | func New(fontAtlas *imgui.ImFontAtlas) *Manager { 59 | imctx := imgui.CreateContext(fontAtlas) 60 | m := &Manager{ 61 | Cache: NewCache(), 62 | Ctx: imctx, 63 | SyncCursor: true, 64 | SyncInputs: true, 65 | ClipMask: true, 66 | ControlCursorShape: true, 67 | inputChars: make([]rune, 0, 256), 68 | } 69 | 70 | // build the texture font atlas 71 | ctx := imgui.GetCurrentContext() 72 | 73 | // GetTexDataAsRGBA32 expects a valid outPixels reference, but all of the 74 | // other parameters can be nil. we don't need the actual pixels, we just 75 | // want it to generate the font atlas 76 | var outPixels []uint32 77 | ctx.IO.Fonts.GetTexDataAsRGBA32(&outPixels, nil, nil, nil) 78 | outPixels = nil 79 | 80 | ctx.IO.Fonts.SetTexID(1) 81 | m.Cache.SetFontAtlasTextureID(1) 82 | 83 | m.setKeyMapping() 84 | return m 85 | } 86 | 87 | // NewWithContext creates a new Manager with a provided ImGui context 88 | func NewWithContext(ctx *imgui.ImGuiContext) *Manager { 89 | m := &Manager{ 90 | Cache: NewCache(), 91 | Ctx: ctx, 92 | SyncCursor: true, 93 | SyncInputs: true, 94 | ClipMask: true, 95 | ControlCursorShape: true, 96 | } 97 | m.setKeyMapping() 98 | return m 99 | } 100 | 101 | func (m *Manager) controlCursorShape() { 102 | if !m.ControlCursorShape { 103 | return 104 | } 105 | 106 | switch imgui.GetMouseCursor() { 107 | case imgui.ImGuiMouseCursor_None: 108 | ebiten.SetCursorShape(ebiten.CursorShapeDefault) 109 | case imgui.ImGuiMouseCursor_Arrow: 110 | ebiten.SetCursorShape(ebiten.CursorShapeDefault) 111 | case imgui.ImGuiMouseCursor_TextInput: 112 | ebiten.SetCursorShape(ebiten.CursorShapeText) 113 | case imgui.ImGuiMouseCursor_ResizeAll: 114 | ebiten.SetCursorShape(ebiten.CursorShapeCrosshair) 115 | case imgui.ImGuiMouseCursor_ResizeEW: 116 | ebiten.SetCursorShape(ebiten.CursorShapeEWResize) 117 | case imgui.ImGuiMouseCursor_ResizeNS: 118 | ebiten.SetCursorShape(ebiten.CursorShapeNSResize) 119 | case imgui.ImGuiMouseCursor_Hand: 120 | ebiten.SetCursorShape(ebiten.CursorShapePointer) 121 | default: 122 | ebiten.SetCursorShape(ebiten.CursorShapeDefault) 123 | } 124 | } 125 | 126 | func (m *Manager) Update(delta float32) { 127 | ctx := imgui.GetCurrentContext() 128 | ctx.IO.DeltaTime = delta 129 | 130 | if m.width > 0 || m.height > 0 { 131 | ctx.IO.DisplaySize = *imgui.NewImVec2(m.width, m.height) 132 | } else if m.screenWidth > 0 || m.screenHeight > 0 { 133 | ctx.IO.DisplaySize = *imgui.NewImVec2(float32(m.screenWidth), float32(m.screenHeight)) 134 | } 135 | 136 | if m.SyncCursor { 137 | // update cursor position 138 | if m.GetCursor != nil { 139 | x, y := m.GetCursor() 140 | ctx.IO.MousePos = *imgui.NewImVec2(x, y) 141 | } else { 142 | mx, my := ebiten.CursorPosition() 143 | ctx.IO.MousePos = *imgui.NewImVec2(float32(mx), float32(my)) 144 | } 145 | 146 | // update mouse buttons 147 | ctx.IO.MouseDown[0] = ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) 148 | ctx.IO.MouseDown[1] = ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) 149 | ctx.IO.MouseDown[2] = ebiten.IsMouseButtonPressed(ebiten.MouseButtonMiddle) 150 | 151 | // update mouse wheel 152 | xoff, yoff := ebiten.Wheel() 153 | ctx.IO.MouseWheel += float32(yoff) 154 | ctx.IO.MouseWheelH += float32(xoff) 155 | m.controlCursorShape() 156 | } 157 | 158 | if m.SyncInputs { 159 | if m.SyncInputsFn != nil { 160 | m.SyncInputsFn() 161 | } else { 162 | m.inputChars = sendInput(&ctx.IO, m.inputChars) 163 | } 164 | } 165 | } 166 | 167 | func (m *Manager) Draw(screen *ebiten.Image) { 168 | m.screenWidth = screen.Bounds().Dx() 169 | m.screenHeight = screen.Bounds().Dy() 170 | imgui.Render() 171 | 172 | if m.ClipMask { 173 | if m.lmask == nil { 174 | w, h := screen.Size() 175 | m.lmask = ebiten.NewImage(w, h) 176 | } else { 177 | w1, h1 := screen.Size() 178 | w2, h2 := m.lmask.Size() 179 | if w1 != w2 || h1 != h2 { 180 | m.lmask.Dispose() 181 | m.lmask = ebiten.NewImage(w1, h1) 182 | } 183 | } 184 | RenderMasked(screen, m.lmask, imgui.GetDrawData(), m.Cache, m.Filter) 185 | } else { 186 | Render(screen, imgui.GetDrawData(), m.Cache, m.Filter) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /renderer/txcache.go: -------------------------------------------------------------------------------- 1 | package renderer 2 | 3 | import ( 4 | "github.com/hajimehoshi/ebiten/v2" 5 | "github.com/zeozeozeo/imgui" 6 | ) 7 | 8 | type TextureCache interface { 9 | FontAtlasTextureID() imgui.ImTextureID 10 | SetFontAtlasTextureID(id imgui.ImTextureID) 11 | GetTexture(id imgui.ImTextureID) *ebiten.Image 12 | SetTexture(id imgui.ImTextureID, img *ebiten.Image) 13 | RemoveTexture(id imgui.ImTextureID) 14 | ResetFontAtlasCache(filter ebiten.Filter) 15 | } 16 | 17 | type textureCache struct { 18 | fontAtlasID imgui.ImTextureID 19 | fontAtlasImage *ebiten.Image 20 | cache map[imgui.ImTextureID]*ebiten.Image 21 | dfilter ebiten.Filter 22 | } 23 | 24 | var _ TextureCache = (*textureCache)(nil) 25 | 26 | func (c *textureCache) getFontAtlas() *ebiten.Image { 27 | if c.fontAtlasImage == nil { 28 | ctx := imgui.GetCurrentContext() 29 | var outPixels []uint32 30 | var outWidth int32 31 | var outHeight int32 32 | ctx.IO.Fonts.GetTexDataAsRGBA32(&outPixels, &outWidth, &outHeight, nil) 33 | c.fontAtlasImage = getTexture(outWidth, outHeight, outPixels) 34 | } 35 | 36 | return c.fontAtlasImage 37 | } 38 | 39 | func (c *textureCache) FontAtlasTextureID() imgui.ImTextureID { 40 | return c.fontAtlasID 41 | } 42 | 43 | func (c *textureCache) SetFontAtlasTextureID(id imgui.ImTextureID) { 44 | c.fontAtlasID = id 45 | // c.fontAtlasImage = nil 46 | } 47 | 48 | func (c *textureCache) GetTexture(id imgui.ImTextureID) *ebiten.Image { 49 | if id != c.fontAtlasID { 50 | if im, ok := c.cache[id]; ok { 51 | return im 52 | } 53 | } 54 | return c.getFontAtlas() 55 | } 56 | 57 | func (c *textureCache) SetTexture(id imgui.ImTextureID, img *ebiten.Image) { 58 | c.cache[id] = img 59 | } 60 | 61 | func (c *textureCache) RemoveTexture(id imgui.ImTextureID) { 62 | delete(c.cache, id) 63 | } 64 | 65 | func (c *textureCache) ResetFontAtlasCache(filter ebiten.Filter) { 66 | c.fontAtlasImage = nil 67 | c.dfilter = filter 68 | } 69 | 70 | func NewCache() TextureCache { 71 | return &textureCache{ 72 | fontAtlasID: 1, 73 | cache: make(map[imgui.ImTextureID]*ebiten.Image), 74 | fontAtlasImage: nil, 75 | } 76 | } 77 | --------------------------------------------------------------------------------