Window Manager Buttons and Bugfixes
* Fix Supervisor event issues wrt. the window manager feature: if a focused window exists and Supervisor is running events for the "other" widgets not in managed windows, and the mouse cursor is over the rectangle of THE focused window, no widget under the cursor receives active (hover, click) events. Prevents being able to click "through" the window and interact with widgets and other windows below. * Adds Close, Maximize and Minimize buttons to windows. Maximize is still buggy and Minimize is implementation-defined behavior with no default event handler configured. * eg/windows has an example of the Window Manager for SDL2 and WebAssembly targets.
This commit is contained in:
parent
7d9ba79cd2
commit
36db160533
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
wasm-tmp
|
||||
*.wasm
|
80
README.md
80
README.md
|
@ -134,7 +134,9 @@ most complex.
|
|||
label next to a small check button. Clicking the label will toggle the
|
||||
state of the checkbox.
|
||||
* [x] **Window**: a Frame with a title bar Frame on top.
|
||||
* Note: Window is not yet draggable or closeable.
|
||||
* Can be managed by Supervisor to give Window Manager controls to it
|
||||
(drag it by its title bar, Close button, window focus, multiple overlapping
|
||||
windows, and so on).
|
||||
* [x] **Tooltip**: a mouse hover label attached to a widget.
|
||||
|
||||
**Work in progress widgets:**
|
||||
|
@ -155,12 +157,6 @@ most complex.
|
|||
|
||||
* [ ] **SelectBox:** a kind of MenuButton that lets the user choose a value
|
||||
from a list of possible values, bound to a string variable.
|
||||
* [ ] **WindowManager**: manages Window widgets and focus support for all
|
||||
interactable widgets.
|
||||
* Would enable Windows to be dragged around by their title bar, overlap
|
||||
other Windows, and rise on top of other Windows when clicked.
|
||||
* Would enable "focus" support for Buttons, Text Boxes and other
|
||||
interactable widgets.
|
||||
* [ ] **TextBox:** an editable text field that the user can focus and type
|
||||
a value into.
|
||||
* Would depend on the WindowManager to manage focus for the widgets.
|
||||
|
@ -206,6 +202,76 @@ state of the widgets under its care.
|
|||
|
||||
The MainWindow includes its own Supervisor, see below.
|
||||
|
||||
## Window Manager
|
||||
|
||||
The ui.Window widget provides a simple frame with a title bar. But, you can
|
||||
use the Supervisor to provide Window Manager controls to your windows!
|
||||
|
||||
The key steps to convert a static Window widget into one that can be dragged
|
||||
around by its title bar are:
|
||||
|
||||
1. Call `window.Supervise(ui.Supervisor)` and give it your Supervisor. It will
|
||||
register itself to be managed by the Supervisor.
|
||||
2. In your main loop, call `Supervisor.Loop()` as you normally would. It
|
||||
handles sending mouse and keyboard events to all managed widgets, including
|
||||
the children of the managed windows.
|
||||
3. In the "draw" part of your main loop, call `Supervisor.Present()` as the
|
||||
final step. Supervisor will draw the managed windows on top of everything
|
||||
else, with the current focused window on top of the others. Note: managed
|
||||
windows are the _only_ widgets drawn by Supervisor; other widgets should be
|
||||
drawn by their parent widgets in their respective Present() methods.
|
||||
|
||||
You can also customize the colors and title bar controls of the managed windows.
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
func example() {
|
||||
engine, _ := sdl.New("Example", 800, 600)
|
||||
supervisor := ui.NewSupervisor()
|
||||
|
||||
win := ui.NewWindow("My Window")
|
||||
|
||||
// Customize the colors of the window. Here are the defaults:
|
||||
win.ActiveTitleBackground = render.Blue
|
||||
win.ActiveTitleForeground = render.White
|
||||
win.InactiveTitleBackground = render.DarkGrey
|
||||
win.InactiveTitleForeground = render.Grey
|
||||
|
||||
// Customize the window buttons by ORing the options.
|
||||
// NOTE: Maximize behavior is still a work in progress, the window doesn't
|
||||
// redraw itself at the new size correctly yet.
|
||||
// NOTE: Minimize button has no default behavior but does trigger a
|
||||
// MinimizeWindow event that you can handle yourself.
|
||||
win.SetButtons(ui.CloseButton | ui.MaximizeButton | ui.MinimizeButton)
|
||||
|
||||
// Add widgets to your window.
|
||||
label := ui.NewLabel(ui.Label{
|
||||
Text: "Hello world!",
|
||||
})
|
||||
win.Pack(label, ui.Pack{
|
||||
Side: ui.W,
|
||||
})
|
||||
|
||||
// Compute the window and its children.
|
||||
win.Compute(engine)
|
||||
|
||||
// This is the key step: give the window to the Supervisor.
|
||||
win.Supervise(supervisor)
|
||||
|
||||
// And in your main loop:
|
||||
// NOTE: MainWindow.MainLoop() does this for you automatically.
|
||||
for {
|
||||
ev, _ = engine.Poll() // poll render engine for mouse/keyboard events
|
||||
supervisor.Loop(ev)
|
||||
supervisor.Present(engine)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See the eg/windows/ example in the git repository for a full example, including
|
||||
SDL2 and WebAssembly versions.
|
||||
|
||||
## MainWindow for Simple Applications
|
||||
|
||||
The MainWindow widget may be used for "simple" UI applications where all you
|
||||
|
|
46
eg/wasm-common/index.html
Normal file
46
eg/wasm-common/index.html
Normal file
|
@ -0,0 +1,46 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>go/ui wasm demo</title>
|
||||
<style>
|
||||
#canvas {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "DejaVuSans";
|
||||
src: url("DejaVuSans.ttf") format("truetype");
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script src="wasm_exec.js"></script>
|
||||
<script>
|
||||
if (!WebAssembly.instantiateStreaming) { // polyfill
|
||||
WebAssembly.instantiateStreaming = async (resp, importObject) => {
|
||||
const source = await (await resp).arrayBuffer();
|
||||
return await WebAssembly.instantiate(source, importObject);
|
||||
};
|
||||
}
|
||||
|
||||
(function() {
|
||||
const go = new Go();
|
||||
WebAssembly.instantiateStreaming(fetch("app.wasm"), go.importObject).then(result => {
|
||||
console.clear();
|
||||
go.run(result.instance);
|
||||
WebAssembly.instantiate(result.module, go.importObject); // reset instance
|
||||
})
|
||||
})();
|
||||
</script>
|
||||
|
||||
<canvas id="canvas"></canvas>
|
||||
|
||||
</body>
|
||||
</html>
|
29
eg/wasm-common/serve.sh
Executable file
29
eg/wasm-common/serve.sh
Executable file
|
@ -0,0 +1,29 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Common build script for WASM examples.
|
||||
# Working directory should be inside a wasm example folder.
|
||||
# This script creates a "./wasm-tmp" folder, copies the wasm-common files
|
||||
# into it, copies your .wasm binary in, and runs server.go pointed to that
|
||||
# directory.
|
||||
#
|
||||
# Read: do not run this script yourself. Run it via a `make wasm-serve` command
|
||||
# in one of the other example directories.
|
||||
#
|
||||
# This probably works best on Linux-like systems only.
|
||||
|
||||
if [[ ! -f "../wasm-common/serve.sh" ]]; then
|
||||
echo Run this script via "make wasm-serve" from a ui/eg example folder.
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -d "./wasm-tmp" ]]; then
|
||||
echo Cleaning ./wasm-tmp folder.
|
||||
rm -rf ./wasm-tmp
|
||||
fi
|
||||
|
||||
mkdir ./wasm-tmp
|
||||
cp ../wasm-common/{wasm_exec.js,index.html} ./wasm-tmp/
|
||||
cp ../DejaVuSans.ttf ./wasm-tmp/
|
||||
cp *.wasm ./wasm-tmp/app.wasm
|
||||
cd wasm-tmp/
|
||||
go run ../../wasm-common/server.go
|
22
eg/wasm-common/server.go
Normal file
22
eg/wasm-common/server.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
// +build disabled
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
const wasm = "/app.wasm"
|
||||
|
||||
http.Handle("/", http.FileServer(http.Dir(".")))
|
||||
http.HandleFunc(wasm, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/wasm")
|
||||
http.ServeFile(w, r, "."+wasm)
|
||||
})
|
||||
|
||||
fmt.Println("Listening at http://localhost:8080/")
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
533
eg/wasm-common/wasm_exec.js
Normal file
533
eg/wasm-common/wasm_exec.js
Normal file
|
@ -0,0 +1,533 @@
|
|||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
(() => {
|
||||
// Map multiple JavaScript environments to a single common API,
|
||||
// preferring web standards over Node.js API.
|
||||
//
|
||||
// Environments considered:
|
||||
// - Browsers
|
||||
// - Node.js
|
||||
// - Electron
|
||||
// - Parcel
|
||||
|
||||
if (typeof global !== "undefined") {
|
||||
// global already exists
|
||||
} else if (typeof window !== "undefined") {
|
||||
window.global = window;
|
||||
} else if (typeof self !== "undefined") {
|
||||
self.global = self;
|
||||
} else {
|
||||
throw new Error("cannot export Go (neither global, window nor self is defined)");
|
||||
}
|
||||
|
||||
if (!global.require && typeof require !== "undefined") {
|
||||
global.require = require;
|
||||
}
|
||||
|
||||
if (!global.fs && global.require) {
|
||||
global.fs = require("fs");
|
||||
}
|
||||
|
||||
if (!global.fs) {
|
||||
let outputBuf = "";
|
||||
global.fs = {
|
||||
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
|
||||
writeSync(fd, buf) {
|
||||
outputBuf += decoder.decode(buf);
|
||||
const nl = outputBuf.lastIndexOf("\n");
|
||||
if (nl != -1) {
|
||||
console.log(outputBuf.substr(0, nl));
|
||||
outputBuf = outputBuf.substr(nl + 1);
|
||||
}
|
||||
return buf.length;
|
||||
},
|
||||
write(fd, buf, offset, length, position, callback) {
|
||||
if (offset !== 0 || length !== buf.length || position !== null) {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
const n = this.writeSync(fd, buf);
|
||||
callback(null, n);
|
||||
},
|
||||
open(path, flags, mode, callback) {
|
||||
const err = new Error("not implemented");
|
||||
err.code = "ENOSYS";
|
||||
callback(err);
|
||||
},
|
||||
read(fd, buffer, offset, length, position, callback) {
|
||||
const err = new Error("not implemented");
|
||||
err.code = "ENOSYS";
|
||||
callback(err);
|
||||
},
|
||||
fsync(fd, callback) {
|
||||
callback(null);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!global.crypto) {
|
||||
const nodeCrypto = require("crypto");
|
||||
global.crypto = {
|
||||
getRandomValues(b) {
|
||||
nodeCrypto.randomFillSync(b);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!global.performance) {
|
||||
global.performance = {
|
||||
now() {
|
||||
const [sec, nsec] = process.hrtime();
|
||||
return sec * 1000 + nsec / 1000000;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!global.TextEncoder) {
|
||||
global.TextEncoder = require("util").TextEncoder;
|
||||
}
|
||||
|
||||
if (!global.TextDecoder) {
|
||||
global.TextDecoder = require("util").TextDecoder;
|
||||
}
|
||||
|
||||
// End of polyfills for common API.
|
||||
|
||||
const encoder = new TextEncoder("utf-8");
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
|
||||
global.Go = class {
|
||||
constructor() {
|
||||
this.argv = ["js"];
|
||||
this.env = {};
|
||||
this.exit = (code) => {
|
||||
if (code !== 0) {
|
||||
console.warn("exit code:", code);
|
||||
}
|
||||
};
|
||||
this._exitPromise = new Promise((resolve) => {
|
||||
this._resolveExitPromise = resolve;
|
||||
});
|
||||
this._pendingEvent = null;
|
||||
this._scheduledTimeouts = new Map();
|
||||
this._nextCallbackTimeoutID = 1;
|
||||
|
||||
const mem = () => {
|
||||
// The buffer may change when requesting more memory.
|
||||
return new DataView(this._inst.exports.mem.buffer);
|
||||
}
|
||||
|
||||
const setInt64 = (addr, v) => {
|
||||
mem().setUint32(addr + 0, v, true);
|
||||
mem().setUint32(addr + 4, Math.floor(v / 4294967296), true);
|
||||
}
|
||||
|
||||
const getInt64 = (addr) => {
|
||||
const low = mem().getUint32(addr + 0, true);
|
||||
const high = mem().getInt32(addr + 4, true);
|
||||
return low + high * 4294967296;
|
||||
}
|
||||
|
||||
const loadValue = (addr) => {
|
||||
const f = mem().getFloat64(addr, true);
|
||||
if (f === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (!isNaN(f)) {
|
||||
return f;
|
||||
}
|
||||
|
||||
const id = mem().getUint32(addr, true);
|
||||
return this._values[id];
|
||||
}
|
||||
|
||||
const storeValue = (addr, v) => {
|
||||
const nanHead = 0x7FF80000;
|
||||
|
||||
if (typeof v === "number") {
|
||||
if (isNaN(v)) {
|
||||
mem().setUint32(addr + 4, nanHead, true);
|
||||
mem().setUint32(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
if (v === 0) {
|
||||
mem().setUint32(addr + 4, nanHead, true);
|
||||
mem().setUint32(addr, 1, true);
|
||||
return;
|
||||
}
|
||||
mem().setFloat64(addr, v, true);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (v) {
|
||||
case undefined:
|
||||
mem().setFloat64(addr, 0, true);
|
||||
return;
|
||||
case null:
|
||||
mem().setUint32(addr + 4, nanHead, true);
|
||||
mem().setUint32(addr, 2, true);
|
||||
return;
|
||||
case true:
|
||||
mem().setUint32(addr + 4, nanHead, true);
|
||||
mem().setUint32(addr, 3, true);
|
||||
return;
|
||||
case false:
|
||||
mem().setUint32(addr + 4, nanHead, true);
|
||||
mem().setUint32(addr, 4, true);
|
||||
return;
|
||||
}
|
||||
|
||||
let ref = this._refs.get(v);
|
||||
if (ref === undefined) {
|
||||
ref = this._values.length;
|
||||
this._values.push(v);
|
||||
this._refs.set(v, ref);
|
||||
}
|
||||
let typeFlag = 0;
|
||||
switch (typeof v) {
|
||||
case "string":
|
||||
typeFlag = 1;
|
||||
break;
|
||||
case "symbol":
|
||||
typeFlag = 2;
|
||||
break;
|
||||
case "function":
|
||||
typeFlag = 3;
|
||||
break;
|
||||
}
|
||||
mem().setUint32(addr + 4, nanHead | typeFlag, true);
|
||||
mem().setUint32(addr, ref, true);
|
||||
}
|
||||
|
||||
const loadSlice = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
|
||||
}
|
||||
|
||||
const loadSliceOfValues = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
const a = new Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
a[i] = loadValue(array + i * 8);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
const loadString = (addr) => {
|
||||
const saddr = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
|
||||
}
|
||||
|
||||
const timeOrigin = Date.now() - performance.now();
|
||||
this.importObject = {
|
||||
go: {
|
||||
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
|
||||
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
|
||||
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
|
||||
// This changes the SP, thus we have to update the SP used by the imported function.
|
||||
|
||||
// func wasmExit(code int32)
|
||||
"runtime.wasmExit": (sp) => {
|
||||
const code = mem().getInt32(sp + 8, true);
|
||||
this.exited = true;
|
||||
delete this._inst;
|
||||
delete this._values;
|
||||
delete this._refs;
|
||||
this.exit(code);
|
||||
},
|
||||
|
||||
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
|
||||
"runtime.wasmWrite": (sp) => {
|
||||
const fd = getInt64(sp + 8);
|
||||
const p = getInt64(sp + 16);
|
||||
const n = mem().getInt32(sp + 24, true);
|
||||
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
|
||||
},
|
||||
|
||||
// func nanotime() int64
|
||||
"runtime.nanotime": (sp) => {
|
||||
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
|
||||
},
|
||||
|
||||
// func walltime() (sec int64, nsec int32)
|
||||
"runtime.walltime": (sp) => {
|
||||
const msec = (new Date).getTime();
|
||||
setInt64(sp + 8, msec / 1000);
|
||||
mem().setInt32(sp + 16, (msec % 1000) * 1000000, true);
|
||||
},
|
||||
|
||||
// func scheduleTimeoutEvent(delay int64) int32
|
||||
"runtime.scheduleTimeoutEvent": (sp) => {
|
||||
const id = this._nextCallbackTimeoutID;
|
||||
this._nextCallbackTimeoutID++;
|
||||
this._scheduledTimeouts.set(id, setTimeout(
|
||||
() => {
|
||||
this._resume();
|
||||
while (this._scheduledTimeouts.has(id)) {
|
||||
// for some reason Go failed to register the timeout event, log and try again
|
||||
// (temporary workaround for https://github.com/golang/go/issues/28975)
|
||||
console.warn("scheduleTimeoutEvent: missed timeout event");
|
||||
this._resume();
|
||||
}
|
||||
},
|
||||
getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
|
||||
));
|
||||
mem().setInt32(sp + 16, id, true);
|
||||
},
|
||||
|
||||
// func clearTimeoutEvent(id int32)
|
||||
"runtime.clearTimeoutEvent": (sp) => {
|
||||
const id = mem().getInt32(sp + 8, true);
|
||||
clearTimeout(this._scheduledTimeouts.get(id));
|
||||
this._scheduledTimeouts.delete(id);
|
||||
},
|
||||
|
||||
// func getRandomData(r []byte)
|
||||
"runtime.getRandomData": (sp) => {
|
||||
crypto.getRandomValues(loadSlice(sp + 8));
|
||||
},
|
||||
|
||||
// func stringVal(value string) ref
|
||||
"syscall/js.stringVal": (sp) => {
|
||||
storeValue(sp + 24, loadString(sp + 8));
|
||||
},
|
||||
|
||||
// func valueGet(v ref, p string) ref
|
||||
"syscall/js.valueGet": (sp) => {
|
||||
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
|
||||
sp = this._inst.exports.getsp(); // see comment above
|
||||
storeValue(sp + 32, result);
|
||||
},
|
||||
|
||||
// func valueSet(v ref, p string, x ref)
|
||||
"syscall/js.valueSet": (sp) => {
|
||||
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
|
||||
},
|
||||
|
||||
// func valueIndex(v ref, i int) ref
|
||||
"syscall/js.valueIndex": (sp) => {
|
||||
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
|
||||
},
|
||||
|
||||
// valueSetIndex(v ref, i int, x ref)
|
||||
"syscall/js.valueSetIndex": (sp) => {
|
||||
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
|
||||
},
|
||||
|
||||
// func valueCall(v ref, m string, args []ref) (ref, bool)
|
||||
"syscall/js.valueCall": (sp) => {
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const m = Reflect.get(v, loadString(sp + 16));
|
||||
const args = loadSliceOfValues(sp + 32);
|
||||
const result = Reflect.apply(m, v, args);
|
||||
sp = this._inst.exports.getsp(); // see comment above
|
||||
storeValue(sp + 56, result);
|
||||
mem().setUint8(sp + 64, 1);
|
||||
} catch (err) {
|
||||
storeValue(sp + 56, err);
|
||||
mem().setUint8(sp + 64, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueInvoke(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueInvoke": (sp) => {
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.apply(v, undefined, args);
|
||||
sp = this._inst.exports.getsp(); // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
mem().setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
storeValue(sp + 40, err);
|
||||
mem().setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueNew(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueNew": (sp) => {
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.construct(v, args);
|
||||
sp = this._inst.exports.getsp(); // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
mem().setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
storeValue(sp + 40, err);
|
||||
mem().setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueLength(v ref) int
|
||||
"syscall/js.valueLength": (sp) => {
|
||||
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
|
||||
},
|
||||
|
||||
// valuePrepareString(v ref) (ref, int)
|
||||
"syscall/js.valuePrepareString": (sp) => {
|
||||
const str = encoder.encode(String(loadValue(sp + 8)));
|
||||
storeValue(sp + 16, str);
|
||||
setInt64(sp + 24, str.length);
|
||||
},
|
||||
|
||||
// valueLoadString(v ref, b []byte)
|
||||
"syscall/js.valueLoadString": (sp) => {
|
||||
const str = loadValue(sp + 8);
|
||||
loadSlice(sp + 16).set(str);
|
||||
},
|
||||
|
||||
// func valueInstanceOf(v ref, t ref) bool
|
||||
"syscall/js.valueInstanceOf": (sp) => {
|
||||
mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16));
|
||||
},
|
||||
|
||||
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||
"syscall/js.copyBytesToGo": (sp) => {
|
||||
const dst = loadSlice(sp + 8);
|
||||
const src = loadValue(sp + 32);
|
||||
if (!(src instanceof Uint8Array)) {
|
||||
mem().setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
mem().setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
// func copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||
"syscall/js.copyBytesToJS": (sp) => {
|
||||
const dst = loadValue(sp + 8);
|
||||
const src = loadSlice(sp + 16);
|
||||
if (!(dst instanceof Uint8Array)) {
|
||||
mem().setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
mem().setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
"debug": (value) => {
|
||||
console.log(value);
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async run(instance) {
|
||||
this._inst = instance;
|
||||
this._values = [ // TODO: garbage collection
|
||||
NaN,
|
||||
0,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
global,
|
||||
this,
|
||||
];
|
||||
this._refs = new Map();
|
||||
this.exited = false;
|
||||
|
||||
const mem = new DataView(this._inst.exports.mem.buffer)
|
||||
|
||||
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
|
||||
let offset = 4096;
|
||||
|
||||
const strPtr = (str) => {
|
||||
const ptr = offset;
|
||||
const bytes = encoder.encode(str + "\0");
|
||||
new Uint8Array(mem.buffer, offset, bytes.length).set(bytes);
|
||||
offset += bytes.length;
|
||||
if (offset % 8 !== 0) {
|
||||
offset += 8 - (offset % 8);
|
||||
}
|
||||
return ptr;
|
||||
};
|
||||
|
||||
const argc = this.argv.length;
|
||||
|
||||
const argvPtrs = [];
|
||||
this.argv.forEach((arg) => {
|
||||
argvPtrs.push(strPtr(arg));
|
||||
});
|
||||
|
||||
const keys = Object.keys(this.env).sort();
|
||||
argvPtrs.push(keys.length);
|
||||
keys.forEach((key) => {
|
||||
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
|
||||
});
|
||||
|
||||
const argv = offset;
|
||||
argvPtrs.forEach((ptr) => {
|
||||
mem.setUint32(offset, ptr, true);
|
||||
mem.setUint32(offset + 4, 0, true);
|
||||
offset += 8;
|
||||
});
|
||||
|
||||
this._inst.exports.run(argc, argv);
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
await this._exitPromise;
|
||||
}
|
||||
|
||||
_resume() {
|
||||
if (this.exited) {
|
||||
throw new Error("Go program has already exited");
|
||||
}
|
||||
this._inst.exports.resume();
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
}
|
||||
|
||||
_makeFuncWrapper(id) {
|
||||
const go = this;
|
||||
return function () {
|
||||
const event = { id: id, this: this, args: arguments };
|
||||
go._pendingEvent = event;
|
||||
go._resume();
|
||||
return event.result;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
global.require &&
|
||||
global.require.main === module &&
|
||||
global.process &&
|
||||
global.process.versions &&
|
||||
!global.process.versions.electron
|
||||
) {
|
||||
if (process.argv.length < 3) {
|
||||
console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const go = new Go();
|
||||
go.argv = process.argv.slice(2);
|
||||
go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
|
||||
go.exit = process.exit;
|
||||
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
|
||||
process.on("exit", (code) => { // Node.js exits if no event handler is pending
|
||||
if (code === 0 && !go.exited) {
|
||||
// deadlock, make Go print error and stack traces
|
||||
go._pendingEvent = { id: 0 };
|
||||
go._resume();
|
||||
}
|
||||
});
|
||||
return go.run(result.instance);
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
})();
|
11
eg/windows/Makefile
Normal file
11
eg/windows/Makefile
Normal file
|
@ -0,0 +1,11 @@
|
|||
.PHONY: run
|
||||
run:
|
||||
go run main.go
|
||||
|
||||
.PHONY: wasm
|
||||
wasm:
|
||||
GOOS=js GOARCH=wasm go build -v -o windows.wasm main_wasm.go
|
||||
|
||||
.PHONY: wasm-serve
|
||||
wasm-serve: wasm
|
||||
../wasm-common/serve.sh
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
|
@ -16,8 +17,22 @@ var (
|
|||
Height = 768
|
||||
|
||||
// Cascade offset for creating multiple windows.
|
||||
Cascade = render.NewPoint(10, 10)
|
||||
CascadeStep = render.NewPoint(24, 24)
|
||||
Cascade = render.NewPoint(10, 10)
|
||||
CascadeStep = render.NewPoint(24, 24)
|
||||
CascadeLoops = 1
|
||||
|
||||
// Colors for each window created.
|
||||
WindowColors = []render.Color{
|
||||
render.Blue,
|
||||
render.Red,
|
||||
render.DarkYellow,
|
||||
render.DarkGreen,
|
||||
render.DarkCyan,
|
||||
render.DarkBlue,
|
||||
render.DarkRed,
|
||||
}
|
||||
WindowID int
|
||||
OpenWindows int
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -31,8 +46,8 @@ func main() {
|
|||
}
|
||||
|
||||
// Add some windows to play with.
|
||||
addWindow(mw, "First window")
|
||||
addWindow(mw, "Second window")
|
||||
addWindow(mw)
|
||||
addWindow(mw)
|
||||
|
||||
mw.SetBackground(render.White)
|
||||
|
||||
|
@ -46,44 +61,50 @@ func main() {
|
|||
}
|
||||
|
||||
// Add a new child window.
|
||||
func addWindow(mw *ui.MainWindow, title string) {
|
||||
func addWindow(mw *ui.MainWindow) {
|
||||
var (
|
||||
color = WindowColors[WindowID%len(WindowColors)]
|
||||
title = fmt.Sprintf("Window %d", WindowID+1)
|
||||
)
|
||||
WindowID++
|
||||
|
||||
win1 := ui.NewWindow(title)
|
||||
win1.SetButtons(ui.CloseButton)
|
||||
win1.ActiveTitleBackground = color
|
||||
win1.InactiveTitleBackground = color.Darken(60)
|
||||
win1.InactiveTitleForeground = render.Grey
|
||||
win1.Configure(ui.Config{
|
||||
Width: 640,
|
||||
Height: 480,
|
||||
Width: 320,
|
||||
Height: 240,
|
||||
})
|
||||
win1.Compute(mw.Engine)
|
||||
win1.Supervise(mw.Supervisor())
|
||||
|
||||
// Attach it to the MainWindow with no placement management, i.e.
|
||||
// instead of Pack() or Place(). Since draggable windows set their own
|
||||
// position, a position manager would only interfere and "snap" the
|
||||
// window back into place as soon as you drop the title bar!
|
||||
// mw.Attach(win1)
|
||||
// Re-open a window when the last one is closed.
|
||||
OpenWindows++
|
||||
win1.Handle(ui.CloseWindow, func(ed ui.EventData) error {
|
||||
OpenWindows--
|
||||
if OpenWindows <= 0 {
|
||||
addWindow(mw)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Default placement via cascade.
|
||||
win1.MoveTo(Cascade)
|
||||
Cascade.Add(CascadeStep)
|
||||
|
||||
// Add a button to the window.
|
||||
// btn := ui.NewButton("Button1", ui.NewLabel(ui.Label{
|
||||
// Text: "Click me!",
|
||||
// }))
|
||||
// btn.Handle(ui.Click, func(ed ui.EventData) {
|
||||
// fmt.Printf("Window '%s' button clicked!\n", title)
|
||||
// })
|
||||
// mw.Add(btn)
|
||||
// win1.Place(btn, ui.Place{
|
||||
// Top: 10,
|
||||
// Left: 10,
|
||||
// })
|
||||
if Cascade.Y > Height-240-64 {
|
||||
CascadeLoops++
|
||||
Cascade.Y = 24
|
||||
Cascade.X = 24 * CascadeLoops
|
||||
}
|
||||
|
||||
// Add a window duplicator button.
|
||||
btn2 := ui.NewButton(title+":Button2", ui.NewLabel(ui.Label{
|
||||
Text: "New Window",
|
||||
}))
|
||||
btn2.Handle(ui.Click, func(ed ui.EventData) error {
|
||||
addWindow(mw, "New Window")
|
||||
addWindow(mw)
|
||||
return nil
|
||||
})
|
||||
mw.Add(btn2)
|
||||
|
|
154
eg/windows/main_wasm.go
Normal file
154
eg/windows/main_wasm.go
Normal file
|
@ -0,0 +1,154 @@
|
|||
// +build js,wasm
|
||||
|
||||
// WebAssembly version of the window manager demo.
|
||||
// To build: make wasm
|
||||
// To test: make wasm-serve
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/render/canvas"
|
||||
"git.kirsle.net/go/ui"
|
||||
)
|
||||
|
||||
// Program globals.
|
||||
var (
|
||||
ThrottleFPS = 1000 / 60
|
||||
|
||||
// Size of the MainWindow.
|
||||
Width = 1024
|
||||
Height = 768
|
||||
|
||||
// Cascade offset for creating multiple windows.
|
||||
Cascade = render.NewPoint(10, 10)
|
||||
CascadeStep = render.NewPoint(24, 24)
|
||||
CascadeLoops = 1
|
||||
|
||||
// Colors for each window created.
|
||||
WindowColors = []render.Color{
|
||||
render.Blue,
|
||||
render.Red,
|
||||
render.DarkYellow,
|
||||
render.DarkGreen,
|
||||
render.DarkCyan,
|
||||
render.DarkBlue,
|
||||
render.DarkRed,
|
||||
}
|
||||
WindowID int
|
||||
OpenWindows int
|
||||
)
|
||||
|
||||
func main() {
|
||||
mw, err := canvas.New("canvas")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Bind DOM event handlers.
|
||||
mw.AddEventListeners()
|
||||
|
||||
supervisor := ui.NewSupervisor()
|
||||
|
||||
frame := ui.NewFrame("Main Frame")
|
||||
frame.Resize(render.NewRect(mw.WindowSize()))
|
||||
frame.Compute(mw)
|
||||
|
||||
_, height := mw.WindowSize()
|
||||
lbl := ui.NewLabel(ui.Label{
|
||||
Text: "Window Manager Demo",
|
||||
Font: render.Text{
|
||||
FontFilename: "DejaVuSans.ttf",
|
||||
Size: 32,
|
||||
Color: render.SkyBlue,
|
||||
Shadow: render.SkyBlue.Darken(60),
|
||||
},
|
||||
})
|
||||
lbl.Compute(mw)
|
||||
lbl.MoveTo(render.NewPoint(
|
||||
20,
|
||||
height-lbl.Size().H-20,
|
||||
))
|
||||
|
||||
// Add some windows to play with.
|
||||
addWindow(mw, frame, supervisor)
|
||||
addWindow(mw, frame, supervisor)
|
||||
|
||||
for {
|
||||
mw.Clear(render.RGBA(255, 255, 200, 255))
|
||||
start := time.Now()
|
||||
ev, err := mw.Poll()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
lbl.Present(mw, lbl.Point())
|
||||
supervisor.Loop(ev)
|
||||
supervisor.Present(mw)
|
||||
|
||||
var delay uint32
|
||||
elapsed := time.Now().Sub(start)
|
||||
tmp := elapsed / time.Millisecond
|
||||
if ThrottleFPS-int(tmp) > 0 {
|
||||
delay = uint32(ThrottleFPS - int(tmp))
|
||||
}
|
||||
mw.Delay(delay)
|
||||
}
|
||||
}
|
||||
|
||||
// Add a new child window.
|
||||
func addWindow(engine render.Engine, parent *ui.Frame, sup *ui.Supervisor) {
|
||||
var (
|
||||
color = WindowColors[WindowID%len(WindowColors)]
|
||||
title = fmt.Sprintf("Window %d", WindowID+1)
|
||||
)
|
||||
WindowID++
|
||||
|
||||
win1 := ui.NewWindow(title)
|
||||
win1.SetButtons(ui.CloseButton)
|
||||
win1.ActiveTitleBackground = color
|
||||
win1.InactiveTitleBackground = color.Darken(60)
|
||||
win1.InactiveTitleForeground = render.Grey
|
||||
win1.Configure(ui.Config{
|
||||
Width: 320,
|
||||
Height: 240,
|
||||
})
|
||||
win1.Compute(engine)
|
||||
win1.Supervise(sup)
|
||||
|
||||
// Re-open a window when the last one is closed.
|
||||
OpenWindows++
|
||||
win1.Handle(ui.CloseWindow, func(ed ui.EventData) error {
|
||||
OpenWindows--
|
||||
if OpenWindows <= 0 {
|
||||
addWindow(engine, parent, sup)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Default placement via cascade.
|
||||
win1.MoveTo(Cascade)
|
||||
Cascade.Add(CascadeStep)
|
||||
if Cascade.Y > Height-240-64 {
|
||||
CascadeLoops++
|
||||
Cascade.Y = 24
|
||||
Cascade.X = 24 * CascadeLoops
|
||||
}
|
||||
|
||||
// Add a window duplicator button.
|
||||
btn2 := ui.NewButton(title+":Button2", ui.NewLabel(ui.Label{
|
||||
Text: "New Window",
|
||||
}))
|
||||
btn2.Handle(ui.Click, func(ed ui.EventData) error {
|
||||
addWindow(engine, parent, sup)
|
||||
return nil
|
||||
})
|
||||
sup.Add(btn2)
|
||||
win1.Place(btn2, ui.Place{
|
||||
Top: 10,
|
||||
Right: 10,
|
||||
})
|
||||
}
|
30
functions.go
30
functions.go
|
@ -25,6 +25,8 @@ func AbsolutePosition(w Widget) render.Point {
|
|||
}
|
||||
|
||||
// AbsoluteRect returns a Rect() offset with the absolute position.
|
||||
// X and Y are the AbsolutePosition of the widget.
|
||||
// W and H are the widget's width and height. (X,Y not added to them)
|
||||
func AbsoluteRect(w Widget) render.Rect {
|
||||
var (
|
||||
P = AbsolutePosition(w)
|
||||
|
@ -33,7 +35,7 @@ func AbsoluteRect(w Widget) render.Rect {
|
|||
return render.Rect{
|
||||
X: P.X,
|
||||
Y: P.Y,
|
||||
W: R.W + P.X,
|
||||
W: R.W,
|
||||
H: R.H, // TODO: the Canvas in EditMode lets you draw pixels
|
||||
// below the status bar if we do `+ R.Y` here.
|
||||
}
|
||||
|
@ -53,7 +55,7 @@ func widgetInFocusedWindow(w Widget) (isManaged, isFocused bool) {
|
|||
for {
|
||||
// Is the node a Window?
|
||||
if window, ok := node.(*Window); ok {
|
||||
return true, window.Focused()
|
||||
return window.managed, window.Focused()
|
||||
}
|
||||
|
||||
node, _ = node.Parent()
|
||||
|
@ -62,27 +64,3 @@ func widgetInFocusedWindow(w Widget) (isManaged, isFocused bool) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WidgetInManagedWindow returns true if the widget is owned by a ui.Window
|
||||
// which is being Window Managed by the Supervisor.
|
||||
//
|
||||
// Returns true if any parent widget is a Window with managed=true. This
|
||||
// boolean is set when you call .Supervise() on the window to be managed by
|
||||
// Supervisor.
|
||||
func WidgetInManagedWindow(w Widget) bool {
|
||||
var node = w
|
||||
|
||||
for {
|
||||
// Is the node a Window?
|
||||
if window, ok := node.(*Window); ok {
|
||||
if window.managed {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
node, _ = node.Parent()
|
||||
if node == nil {
|
||||
return false // reached the root
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -164,7 +164,6 @@ func (mw *MainWindow) MainLoop() error {
|
|||
|
||||
// Loop does one loop of the UI.
|
||||
func (mw *MainWindow) Loop() error {
|
||||
fmt.Printf("------ MAIN LOOP\n")
|
||||
mw.Engine.Clear(render.White)
|
||||
|
||||
// Record how long this loop took.
|
||||
|
|
|
@ -28,6 +28,11 @@ const (
|
|||
DragMove // mouse movements sent to a widget being dragged.
|
||||
Drop // a "drop site" widget under the cursor when a drag is done
|
||||
|
||||
// Window Manager events.
|
||||
CloseWindow
|
||||
MaximizeWindow
|
||||
MinimizeWindow
|
||||
|
||||
// Lifecycle event handlers.
|
||||
Compute // fired whenever the widget runs Compute
|
||||
Present // fired whenever the widget runs Present
|
||||
|
@ -149,6 +154,18 @@ func (s *Supervisor) Loop(ev *event.State) error {
|
|||
return ErrStopPropagation
|
||||
}
|
||||
|
||||
// Check if the top focused window has been closed and auto-focus the next.
|
||||
if s.winFocus != nil && s.winFocus.window.Hidden() {
|
||||
next := s.winFocus.next
|
||||
for next != nil {
|
||||
if !next.window.Hidden() {
|
||||
s.FocusWindow(next.window)
|
||||
break
|
||||
}
|
||||
next = next.next
|
||||
}
|
||||
}
|
||||
|
||||
// Run events in managed windows first, from top to bottom.
|
||||
// Widgets in unmanaged windows will be handled next.
|
||||
// err := s.runWindowEvents(XY, ev, hovering, outside)
|
||||
|
@ -223,6 +240,18 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State, hovering,
|
|||
ranEvents bool
|
||||
)
|
||||
|
||||
// If we're running this method in "Phase 2" (to widgets NOT in the focused
|
||||
// window), only send mouse events to widgets if the cursor is NOT inside
|
||||
// the bounding box of the active focused window. Prevents clicking "thru"
|
||||
// the window and activating widgets/other windows behind it.
|
||||
var cursorInsideFocusedWindow bool
|
||||
if !toFocusedWindow && s.winFocus != nil {
|
||||
// Get the bounding box of the focused window.
|
||||
if XY.Inside(AbsoluteRect(s.winFocus.window)) {
|
||||
cursorInsideFocusedWindow = true
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for an Event response errors.
|
||||
handle := func(err error) {
|
||||
// Did any event handler run?
|
||||
|
@ -241,6 +270,13 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State, hovering,
|
|||
break
|
||||
}
|
||||
|
||||
// If the cursor is inside the box of the focused window, don't trigger
|
||||
// active (hovering) mouse events. MouseOut type events, below, can still
|
||||
// trigger.
|
||||
if cursorInsideFocusedWindow {
|
||||
break
|
||||
}
|
||||
|
||||
var (
|
||||
id = child.id
|
||||
w = child.widget
|
||||
|
@ -270,7 +306,7 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State, hovering,
|
|||
}
|
||||
|
||||
// It is a window, but can only be the non-focused window.
|
||||
if window.focused {
|
||||
if isWindow && window.focused {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
|
|
@ -494,7 +494,10 @@ func (w *BaseWidget) Present(e render.Engine, p render.Point) {
|
|||
func (w *BaseWidget) Event(event Event, e EventData) error {
|
||||
if handlers, ok := w.handlers[event]; ok {
|
||||
for _, fn := range handlers {
|
||||
return fn(e)
|
||||
res := fn(e)
|
||||
if res == ErrStopPropagation {
|
||||
return res
|
||||
}
|
||||
}
|
||||
}
|
||||
return ErrNoEventHandler
|
||||
|
|
154
window.go
154
window.go
|
@ -9,8 +9,7 @@ import (
|
|||
// Window is a frame with a title bar.
|
||||
type Window struct {
|
||||
BaseWidget
|
||||
Title string
|
||||
Active bool
|
||||
Title string
|
||||
|
||||
// Title bar colors. Sensible defaults are chosen in NewWindow but you
|
||||
// may customize after the fact.
|
||||
|
@ -20,17 +19,25 @@ type Window struct {
|
|||
InactiveTitleForeground render.Color
|
||||
|
||||
// Private widgets.
|
||||
body *Frame
|
||||
titleBar *Frame
|
||||
titleLabel *Label
|
||||
content *Frame
|
||||
body *Frame
|
||||
titleBar *Frame
|
||||
titleLabel *Label
|
||||
titleButtons []*Button
|
||||
content *Frame
|
||||
|
||||
// Configured title bar buttons.
|
||||
buttonsEnabled int
|
||||
|
||||
// Window manager controls.
|
||||
dragging bool
|
||||
startDragAt render.Point // cursor position when drag began
|
||||
dragOrigPoint render.Point // original position of window at drag start
|
||||
focused bool
|
||||
managed bool // window is managed by Supervisor
|
||||
managed bool // window is managed by Supervisor
|
||||
maximized bool // toggled by MaximizeButton
|
||||
origPoint render.Point // placement before a maximize
|
||||
origSize render.Rect // size before a maximize
|
||||
engine render.Engine // hang onto the render engine, for Maximize support.
|
||||
}
|
||||
|
||||
// NewWindow creates a new window.
|
||||
|
@ -42,8 +49,8 @@ func NewWindow(title string) *Window {
|
|||
// Default title bar colors.
|
||||
ActiveTitleBackground: render.Blue,
|
||||
ActiveTitleForeground: render.White,
|
||||
InactiveTitleBackground: render.Grey,
|
||||
InactiveTitleForeground: render.Black,
|
||||
InactiveTitleBackground: render.DarkGrey,
|
||||
InactiveTitleForeground: render.Grey,
|
||||
}
|
||||
w.IDFunc(func() string {
|
||||
return fmt.Sprintf("Window<%s %+v>",
|
||||
|
@ -85,11 +92,12 @@ func NewWindow(title string) *Window {
|
|||
|
||||
// setupTitlebar creates the title bar frame of the window.
|
||||
func (w *Window) setupTitleBar() (*Frame, *Label) {
|
||||
frame := NewFrame("Titlebar for Windows: " + w.Title)
|
||||
frame := NewFrame("Titlebar for Window: " + w.Title)
|
||||
frame.Configure(Config{
|
||||
Background: w.ActiveTitleBackground,
|
||||
})
|
||||
|
||||
// Title label.
|
||||
label := NewLabel(Label{
|
||||
TextVariable: &w.Title,
|
||||
Font: render.Text{
|
||||
|
@ -103,9 +111,91 @@ func (w *Window) setupTitleBar() (*Frame, *Label) {
|
|||
Side: W,
|
||||
})
|
||||
|
||||
// Window buttons.
|
||||
var buttons = []struct {
|
||||
If bool
|
||||
Label string
|
||||
Event Event
|
||||
}{
|
||||
{
|
||||
Label: "×",
|
||||
Event: CloseWindow,
|
||||
},
|
||||
{
|
||||
Label: "+",
|
||||
Event: MaximizeWindow,
|
||||
},
|
||||
{
|
||||
Label: "_",
|
||||
Event: MinimizeWindow,
|
||||
},
|
||||
}
|
||||
w.titleButtons = make([]*Button, len(buttons))
|
||||
for i, cfg := range buttons {
|
||||
cfg := cfg
|
||||
btn := NewButton(
|
||||
fmt.Sprintf("Title Button %d for Window: %s", i, w.Title),
|
||||
NewLabel(Label{
|
||||
Text: cfg.Label,
|
||||
Font: render.Text{
|
||||
// Color: w.ActiveTitleForeground,
|
||||
Size: 8,
|
||||
Padding: 2,
|
||||
},
|
||||
}),
|
||||
)
|
||||
btn.SetBorderSize(0)
|
||||
btn.Handle(Click, func(ed EventData) error {
|
||||
w.Event(cfg.Event, ed)
|
||||
return ErrStopPropagation // TODO: doesn't work :(
|
||||
})
|
||||
btn.Hide()
|
||||
w.titleButtons[i] = btn
|
||||
|
||||
frame.Pack(btn, Pack{
|
||||
Side: E,
|
||||
})
|
||||
}
|
||||
|
||||
return frame, label
|
||||
}
|
||||
|
||||
// SetButtons sets the title bar buttons to show in the window.
|
||||
//
|
||||
// The value should be the OR of CloseButton, MaximizeButton and MinimizeButton
|
||||
// that you want to be enabled.
|
||||
//
|
||||
// Window buttons only work if the window is managed by Supervisor and you have
|
||||
// called the Supervise() method of the window.
|
||||
func (w *Window) SetButtons(buttons int) {
|
||||
// Show/hide each button based on the value given.
|
||||
var toggle = []struct {
|
||||
Value int
|
||||
Index int
|
||||
}{
|
||||
{
|
||||
Value: CloseButton,
|
||||
Index: 0,
|
||||
},
|
||||
{
|
||||
Value: MaximizeButton,
|
||||
Index: 1,
|
||||
},
|
||||
{
|
||||
Value: MinimizeButton,
|
||||
Index: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, item := range toggle {
|
||||
if buttons&item.Value == item.Value {
|
||||
w.titleButtons[item.Index].Show()
|
||||
} else {
|
||||
w.titleButtons[item.Index].Hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Supervise enables the window to be dragged around by its title bar by
|
||||
// adding its relevant event hooks to your Supervisor.
|
||||
func (w *Window) Supervise(s *Supervisor) {
|
||||
|
@ -113,7 +203,6 @@ func (w *Window) Supervise(s *Supervisor) {
|
|||
w.titleBar.Handle(MouseDown, func(ed EventData) error {
|
||||
w.startDragAt = ed.Point
|
||||
w.dragOrigPoint = w.Point()
|
||||
fmt.Printf("Clicked at %s window at %s!\n", ed.Point, w.dragOrigPoint)
|
||||
|
||||
s.DragStartWidget(w)
|
||||
return nil
|
||||
|
@ -122,10 +211,6 @@ func (w *Window) Supervise(s *Supervisor) {
|
|||
// Clicking anywhere in the window focuses the window.
|
||||
w.Handle(MouseDown, func(ed EventData) error {
|
||||
s.FocusWindow(w)
|
||||
fmt.Printf("%s handles click event\n", w)
|
||||
return nil
|
||||
})
|
||||
w.Handle(Click, func(ed EventData) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
|
@ -134,7 +219,6 @@ func (w *Window) Supervise(s *Supervisor) {
|
|||
// Get the delta of movement from where we began.
|
||||
delta := w.startDragAt.Compare(ed.Point)
|
||||
if delta != render.Origin {
|
||||
fmt.Printf(" Dragged to: %s Delta: %s\n", ed.Point, delta)
|
||||
moveTo := w.dragOrigPoint
|
||||
moveTo.Add(delta)
|
||||
w.MoveTo(moveTo)
|
||||
|
@ -142,8 +226,21 @@ func (w *Window) Supervise(s *Supervisor) {
|
|||
return nil
|
||||
})
|
||||
|
||||
// Window button handlers.
|
||||
w.Handle(CloseWindow, func(ed EventData) error {
|
||||
w.Hide()
|
||||
return nil
|
||||
})
|
||||
w.Handle(MaximizeWindow, func(ed EventData) error {
|
||||
w.SetMaximized(!w.maximized)
|
||||
return nil
|
||||
})
|
||||
|
||||
// Add the title bar to the supervisor.
|
||||
s.Add(w.titleBar)
|
||||
for _, btn := range w.titleButtons {
|
||||
s.Add(btn)
|
||||
}
|
||||
s.Add(w)
|
||||
|
||||
// Add the window to the focus list of the supervisor.
|
||||
|
@ -175,6 +272,30 @@ func (w *Window) SetFocus(v bool) {
|
|||
w.titleLabel.Font.Stroke = bg.Darken(40)
|
||||
}
|
||||
|
||||
// Maximized returns whether the window is maximized.
|
||||
func (w *Window) Maximized() bool {
|
||||
return w.maximized
|
||||
}
|
||||
|
||||
// SetMaximized sets the state of the maximized window.
|
||||
// Must have called Compute() once before so the window can hang on to the
|
||||
// render.Engine, to calculate the size of the parent window.
|
||||
func (w *Window) SetMaximized(v bool) {
|
||||
w.maximized = v
|
||||
|
||||
if v && w.engine != nil {
|
||||
w.origPoint = w.Point()
|
||||
w.origSize = w.Size()
|
||||
w.MoveTo(render.Origin)
|
||||
w.Resize(render.NewRect(w.engine.WindowSize()))
|
||||
w.Compute(w.engine)
|
||||
} else if w.engine != nil {
|
||||
w.MoveTo(w.origPoint)
|
||||
w.Resize(w.origSize)
|
||||
w.Compute(w.engine)
|
||||
}
|
||||
}
|
||||
|
||||
// Children returns the window's child widgets.
|
||||
func (w *Window) Children() []Widget {
|
||||
return []Widget{
|
||||
|
@ -216,6 +337,7 @@ func (w *Window) ConfigureTitle(C Config) {
|
|||
|
||||
// Compute the window.
|
||||
func (w *Window) Compute(e render.Engine) {
|
||||
w.engine = e // hang onto it in case of maximize
|
||||
w.body.Compute(e)
|
||||
|
||||
// Call the BaseWidget Compute in case we have subscribers.
|
||||
|
|
|
@ -12,6 +12,19 @@ window_manager.go holds data types and Supervisor methods related to the
|
|||
management of ui.Window widgets.
|
||||
*/
|
||||
|
||||
// Window button options. OR these together in a call to Window.SetButtons().
|
||||
const (
|
||||
CloseButton = 0x01
|
||||
|
||||
// NOTICE: MaximizeButton behavior is currently buggy, window doesn't
|
||||
// redraw itself at the new size properly.
|
||||
MaximizeButton = 0x02
|
||||
|
||||
// Minimize button has no default behavior attached; you can bind it with
|
||||
// window.Handle(MinimizeWindow) to set your own event handler.
|
||||
MinimizeButton = 0x04
|
||||
)
|
||||
|
||||
// FocusedWindow is a doubly-linked list of recently focused Windows, with
|
||||
// the current and most-recently focused on top. TODO make not exported.
|
||||
type FocusedWindow struct {
|
||||
|
@ -69,15 +82,6 @@ func (s *Supervisor) addWindow(win *Window) {
|
|||
}
|
||||
}
|
||||
|
||||
// presentWindows draws the windows from bottom to top.
|
||||
func (s *Supervisor) presentWindows(e render.Engine) {
|
||||
item := s.winBottom
|
||||
for item != nil {
|
||||
item.window.Present(e, item.window.Point())
|
||||
item = item.prev
|
||||
}
|
||||
}
|
||||
|
||||
// FocusWindow brings the given window to the top of the supervisor's focus.
|
||||
//
|
||||
// The window must have previously been added to the supervisor's Window Manager
|
||||
|
@ -146,3 +150,13 @@ func (s *Supervisor) FocusWindow(win *Window) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// presentWindows draws the windows from bottom to top.
|
||||
func (s *Supervisor) presentWindows(e render.Engine) {
|
||||
item := s.winBottom
|
||||
for item != nil {
|
||||
item.window.Compute(e)
|
||||
item.window.Present(e, item.window.Point())
|
||||
item = item.prev
|
||||
}
|
||||
}
|
||||
|
|
51
window_test.go
Normal file
51
window_test.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package ui_test
|
||||
|
||||
import (
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/ui"
|
||||
)
|
||||
|
||||
// Example of using the Supervisor Window Manager.
|
||||
func ExampleWindow() {
|
||||
mw, err := ui.NewMainWindow("Window Manager Example", 800, 600)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create a window as normal.
|
||||
window := ui.NewWindow("Hello world!")
|
||||
window.Configure(ui.Config{
|
||||
Width: 320,
|
||||
Height: 240,
|
||||
})
|
||||
|
||||
// Configure its title bar colors (optional; these are the defaults)
|
||||
window.ActiveTitleBackground = render.Blue
|
||||
window.ActiveTitleForeground = render.White
|
||||
window.InactiveTitleBackground = render.DarkGrey
|
||||
window.InactiveTitleForeground = render.Grey
|
||||
|
||||
// Configure its window buttons (optional); default has no window buttons.
|
||||
// Window buttons are only functional in managed windows.
|
||||
window.SetButtons(ui.CloseButton | ui.MaximizeButton | ui.MinimizeButton)
|
||||
|
||||
// Add some widgets to the window.
|
||||
btn := ui.NewButton("My Button", ui.NewLabel(ui.Label{
|
||||
Text: "Hello world!",
|
||||
}))
|
||||
window.Place(btn, ui.Place{
|
||||
Center: true,
|
||||
Middle: true,
|
||||
})
|
||||
|
||||
// To enable the window manager controls, the key step is to give it
|
||||
// the Supervisor so it can be managed:
|
||||
window.Compute(mw.Engine)
|
||||
window.Supervise(mw.Supervisor())
|
||||
|
||||
// Each loop you must then:
|
||||
// - Call Supervisor.Loop() as normal to handle events.
|
||||
// - Call Supervisor.Present() to draw the managed windows.
|
||||
// MainLoop() of the MainWindow does this for you.
|
||||
mw.MainLoop()
|
||||
}
|
Loading…
Reference in New Issue
Block a user