Merge pull request 'Menus and Menu Bars' (#2) from menus into master
This commit is contained in:
commit
53c0fed7be
18
README.md
18
README.md
|
@ -138,25 +138,21 @@ most complex.
|
|||
(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.
|
||||
* [x] **MenuButton**: a button that opens a modal pop-up menu on click.
|
||||
* [x] **MenuBar**: a specialized Frame that groups a bunch of MenuButtons and
|
||||
provides a simple API to add menus and items to it.
|
||||
* [x] **Menu**: a frame full of clickable links and separators. Usually used as
|
||||
a modal pop-up by the MenuButton and MenuBar.
|
||||
|
||||
**Work in progress widgets:**
|
||||
|
||||
* [x] **Menu**: a frame with clickable menu items.
|
||||
* To be a base widget behind right-click context menus, pull-down menus
|
||||
from a MenuBar, options from a SelectBox and so on.
|
||||
* Powered by Frame and Button but with a nice API for composing menu
|
||||
actions.
|
||||
* Partially implemented so far.
|
||||
* [ ] **MenuButton**: a Button that opens a Menu when clicked.
|
||||
* [ ] **MenuBar**: a Frame that houses many MenuButtons, intended for the
|
||||
main menu at the top of a UI window (File, Edit, Help, etc.).
|
||||
* [ ] **Scrollbar**: a Frame including a trough, scroll buttons and a
|
||||
draggable slider.
|
||||
* [ ] **SelectBox:** a kind of MenuButton that lets the user choose a value
|
||||
from a list of possible values, bound to a string variable.
|
||||
|
||||
**Wish list for the longer-term future:**
|
||||
|
||||
* [ ] **SelectBox:** a kind of MenuButton that lets the user choose a value
|
||||
from a list of possible values, bound to a string variable.
|
||||
* [ ] **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.
|
||||
|
|
8
debug.go
8
debug.go
|
@ -5,6 +5,14 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// PrintWidgetTree prints a widget tree to console.
|
||||
func PrintWidgetTree(root Widget) {
|
||||
fmt.Printf("--- Widget Tree of %s ---\n", root)
|
||||
for _, row := range WidgetTree(root) {
|
||||
fmt.Println(row)
|
||||
}
|
||||
}
|
||||
|
||||
// WidgetTree returns a string representing the tree of widgets starting
|
||||
// at a given widget.
|
||||
func WidgetTree(root Widget) []string {
|
||||
|
|
BIN
docs/menus-1.png
Normal file
BIN
docs/menus-1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
BIN
docs/menus-2.png
Normal file
BIN
docs/menus-2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
10
eg/README.md
Normal file
10
eg/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
# Examples for go/ui
|
||||
|
||||
* [Hello, World!](hello-world/): a basic UI demo.
|
||||
* [Frame Place()](frame-place/): demonstrates using the Place() layout management
|
||||
option for Frame widgets.]
|
||||
* [Window Manager](windows/): demonstrates the Window widget and window
|
||||
management features of the Supervisor.
|
||||
* [Tooltip](tooltip/): demonstrates the Tooltip widget on a variety of buttons
|
||||
scattered around the window.
|
||||
* [Menus](menus/): demonstrates various Menu Buttons and a Menu Bar.
|
11
eg/menus/Makefile
Normal file
11
eg/menus/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
|
19
eg/menus/README.md
Normal file
19
eg/menus/README.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Menu Example
|
||||
|
||||
This example shows off the Menu, MenuButton, and MenuBar widgets.
|
||||
|
||||
* MenuButton is your basic button that pops up a Menu when clicked.
|
||||
* MenuBar is a specialized Frame that attaches to the top of the parent
|
||||
(usually the window) and provides a simple API to add menus and items.
|
||||
* Menu is the underlying "pop-up and select an item" widget.
|
||||
|
||||
## Running It
|
||||
|
||||
From your terminal, just type `go run main.go` or `make run` from this
|
||||
example's directory.
|
||||
|
||||
## Screenshots
|
||||
|
||||
![Screenshot 1](../../docs/menus-1.png)
|
||||
|
||||
![Screenshot 2](../../docs/menus-2.png)
|
208
eg/menus/main.go
Normal file
208
eg/menus/main.go
Normal file
|
@ -0,0 +1,208 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/render/event"
|
||||
"git.kirsle.net/go/render/sdl"
|
||||
"git.kirsle.net/go/ui"
|
||||
)
|
||||
|
||||
// Program globals.
|
||||
var (
|
||||
// Size of the MainWindow.
|
||||
Width = 640
|
||||
Height = 480
|
||||
|
||||
BGColor = render.White
|
||||
)
|
||||
|
||||
func init() {
|
||||
sdl.DefaultFontFilename = "../DejaVuSans.ttf"
|
||||
}
|
||||
|
||||
func main() {
|
||||
mw, err := ui.NewMainWindow("Menu Demo", Width, Height)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
setupMainMenu(mw)
|
||||
|
||||
// Menu button in middle of window.
|
||||
{
|
||||
btn := ui.NewMenuButton("MenuBtn", ui.NewLabel(ui.Label{
|
||||
Text: "Click me!",
|
||||
}))
|
||||
btn.Supervise(mw.Supervisor())
|
||||
mw.Place(btn, ui.Place{
|
||||
Center: true,
|
||||
Middle: true,
|
||||
})
|
||||
|
||||
for _, label := range []string{
|
||||
"MenuButtons open menus",
|
||||
"when clicked. MenuBar is ",
|
||||
"a Frame of MenuButtons",
|
||||
"attached to the top of the",
|
||||
"window.",
|
||||
"",
|
||||
"They all provide a nice API",
|
||||
"to insert menus and items.",
|
||||
} {
|
||||
label := label
|
||||
if label == "" {
|
||||
btn.AddSeparator()
|
||||
continue
|
||||
}
|
||||
btn.AddItem(label, func() {
|
||||
fmt.Printf("Button '%s' clicked!\n", label)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Menu button on the bottom right of screen.
|
||||
{
|
||||
btn := ui.NewMenuButton("BrBtn", ui.NewLabel(ui.Label{
|
||||
Text: "Fruits",
|
||||
}))
|
||||
btn.Supervise(mw.Supervisor())
|
||||
mw.Place(btn, ui.Place{
|
||||
Right: 20,
|
||||
Bottom: 20,
|
||||
})
|
||||
|
||||
btn.AddItem("Apples", func() {})
|
||||
btn.AddItem("Oranges", func() {})
|
||||
btn.AddItem("Bananas", func() {})
|
||||
btn.AddItem("Pears", func() {})
|
||||
}
|
||||
|
||||
// Menu button on the bottom left of screen.
|
||||
{
|
||||
btn := ui.NewMenuButton("BlBtn", ui.NewLabel(ui.Label{
|
||||
Text: "Set Window Color",
|
||||
}))
|
||||
btn.Supervise(mw.Supervisor())
|
||||
mw.Place(btn, ui.Place{
|
||||
Left: 20,
|
||||
Bottom: 20,
|
||||
})
|
||||
|
||||
setBg := func(color render.Color) func() {
|
||||
return func() {
|
||||
BGColor = color
|
||||
}
|
||||
}
|
||||
|
||||
// Really fancy buttons.
|
||||
var colors = []struct {
|
||||
label string
|
||||
hex string
|
||||
color render.Color
|
||||
}{
|
||||
{"Black", "#000", render.Black},
|
||||
{"Red", "#F00", render.Red},
|
||||
{"Yellow", "#FF0", render.Yellow},
|
||||
{"Green", "#0F0", render.Green},
|
||||
{"Cyan", "#0FF", render.Cyan},
|
||||
{"Blue", "#00F", render.Blue},
|
||||
{"Magenta", "#F0F", render.Magenta},
|
||||
{"White", "#FFF", render.White},
|
||||
}
|
||||
for _, opt := range colors {
|
||||
item := btn.AddItemAccel(opt.label, opt.hex, setBg(opt.color))
|
||||
item.SetBackground(opt.color.Lighten(128))
|
||||
}
|
||||
|
||||
// btn.AddItemAccel("Black", "#000", setBg(render.White))
|
||||
// btn.AddItemAccel("Red", "#F00", setBg(render.Red))
|
||||
// btn.AddItemAccel("Yellow", "#FF0", setBg(render.Yellow))
|
||||
// btn.AddItemAccel("Green", "#0F0", setBg(render.Green))
|
||||
// btn.AddItemAccel("Cyan", "#0FF", setBg(render.Cyan))
|
||||
// btn.AddItemAccel("Blue", "#00F", setBg(render.Blue))
|
||||
// btn.AddItemAccel("Magenta", "#F0F", setBg(render.Magenta))
|
||||
// btn.AddItemAccel("White", "#FFF", setBg(render.White))
|
||||
}
|
||||
|
||||
// The "Long Menu" on the middle left side
|
||||
{
|
||||
btn := ui.NewMenuButton("BlBtn", ui.NewLabel(ui.Label{
|
||||
Text: "Tall Growing Menu",
|
||||
}))
|
||||
btn.Supervise(mw.Supervisor())
|
||||
mw.Place(btn, ui.Place{
|
||||
Left: 20,
|
||||
Middle: true,
|
||||
})
|
||||
|
||||
var id int
|
||||
btn.AddItem("Add New Option", func() {
|
||||
id++
|
||||
id := id
|
||||
btn.AddItem(fmt.Sprintf("Menu Item #%d", id), func() {
|
||||
fmt.Printf("Chosen menu item %d\n", id)
|
||||
})
|
||||
})
|
||||
|
||||
btn.AddSeparator()
|
||||
}
|
||||
|
||||
mw.OnLoop(func(e *event.State) {
|
||||
mw.SetBackground(BGColor)
|
||||
if e.Up {
|
||||
fmt.Println("Supervised widgets:")
|
||||
for widg := range mw.Supervisor().Widgets() {
|
||||
fmt.Printf("%+v\n", widg)
|
||||
}
|
||||
}
|
||||
if e.Escape {
|
||||
os.Exit(0)
|
||||
}
|
||||
})
|
||||
|
||||
mw.MainLoop()
|
||||
}
|
||||
|
||||
func setupMainMenu(mw *ui.MainWindow) {
|
||||
bar := ui.NewMenuBar("Main Menu")
|
||||
|
||||
fileMenu := bar.AddMenu("File")
|
||||
fileMenu.AddItemAccel("New", "Ctrl-N", func() {
|
||||
fmt.Println("Chose File->New")
|
||||
})
|
||||
fileMenu.AddItemAccel("Open", "Ctrl-O", func() {
|
||||
fmt.Println("Chose File->Open")
|
||||
})
|
||||
fileMenu.AddSeparator()
|
||||
fileMenu.AddItemAccel("Exit", "Alt-F4", func() {
|
||||
fmt.Println("Chose File->Exit")
|
||||
os.Exit(0)
|
||||
})
|
||||
|
||||
editMenu := bar.AddMenu("Edit")
|
||||
editMenu.AddItemAccel("Undo", "Ctrl-Z", func() {})
|
||||
editMenu.AddItemAccel("Redo", "Shift-Ctrl-Z", func() {})
|
||||
editMenu.AddSeparator()
|
||||
editMenu.AddItemAccel("Cut", "Ctrl-X", func() {})
|
||||
editMenu.AddItemAccel("Copy", "Ctrl-C", func() {})
|
||||
editMenu.AddItemAccel("Paste", "Ctrl-V", func() {})
|
||||
editMenu.AddSeparator()
|
||||
editMenu.AddItem("Settings...", func() {})
|
||||
|
||||
viewMenu := bar.AddMenu("View")
|
||||
viewMenu.AddItemAccel("Toggle Full Screen", "F11", func() {})
|
||||
|
||||
helpMenu := bar.AddMenu("Help")
|
||||
helpMenu.AddItemAccel("Contents", "F1", func() {})
|
||||
helpMenu.AddItem("About", func() {})
|
||||
|
||||
bar.Supervise(mw.Supervisor())
|
||||
bar.Compute(mw.Engine)
|
||||
mw.Pack(bar, bar.PackTop())
|
||||
|
||||
fmt.Printf("Setup MenuBar: %s\n", bar.Size())
|
||||
}
|
|
@ -30,6 +30,12 @@
|
|||
global.fs = require("fs");
|
||||
}
|
||||
|
||||
const enosys = () => {
|
||||
const err = new Error("not implemented");
|
||||
err.code = "ENOSYS";
|
||||
return err;
|
||||
};
|
||||
|
||||
if (!global.fs) {
|
||||
let outputBuf = "";
|
||||
global.fs = {
|
||||
|
@ -45,27 +51,53 @@
|
|||
},
|
||||
write(fd, buf, offset, length, position, callback) {
|
||||
if (offset !== 0 || length !== buf.length || position !== null) {
|
||||
throw new Error("not implemented");
|
||||
callback(enosys());
|
||||
return;
|
||||
}
|
||||
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);
|
||||
},
|
||||
chmod(path, mode, callback) { callback(enosys()); },
|
||||
chown(path, uid, gid, callback) { callback(enosys()); },
|
||||
close(fd, callback) { callback(enosys()); },
|
||||
fchmod(fd, mode, callback) { callback(enosys()); },
|
||||
fchown(fd, uid, gid, callback) { callback(enosys()); },
|
||||
fstat(fd, callback) { callback(enosys()); },
|
||||
fsync(fd, callback) { callback(null); },
|
||||
ftruncate(fd, length, callback) { callback(enosys()); },
|
||||
lchown(path, uid, gid, callback) { callback(enosys()); },
|
||||
link(path, link, callback) { callback(enosys()); },
|
||||
lstat(path, callback) { callback(enosys()); },
|
||||
mkdir(path, perm, callback) { callback(enosys()); },
|
||||
open(path, flags, mode, callback) { callback(enosys()); },
|
||||
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
|
||||
readdir(path, callback) { callback(enosys()); },
|
||||
readlink(path, callback) { callback(enosys()); },
|
||||
rename(from, to, callback) { callback(enosys()); },
|
||||
rmdir(path, callback) { callback(enosys()); },
|
||||
stat(path, callback) { callback(enosys()); },
|
||||
symlink(path, link, callback) { callback(enosys()); },
|
||||
truncate(path, length, callback) { callback(enosys()); },
|
||||
unlink(path, callback) { callback(enosys()); },
|
||||
utimes(path, atime, mtime, callback) { callback(enosys()); },
|
||||
};
|
||||
}
|
||||
|
||||
if (!global.process) {
|
||||
global.process = {
|
||||
getuid() { return -1; },
|
||||
getgid() { return -1; },
|
||||
geteuid() { return -1; },
|
||||
getegid() { return -1; },
|
||||
getgroups() { throw enosys(); },
|
||||
pid: -1,
|
||||
ppid: -1,
|
||||
umask() { throw enosys(); },
|
||||
cwd() { throw enosys(); },
|
||||
chdir() { throw enosys(); },
|
||||
}
|
||||
}
|
||||
|
||||
if (!global.crypto) {
|
||||
const nodeCrypto = require("crypto");
|
||||
global.crypto = {
|
||||
|
@ -113,24 +145,19 @@
|
|||
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);
|
||||
this.mem.setUint32(addr + 0, v, true);
|
||||
this.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);
|
||||
const low = this.mem.getUint32(addr + 0, true);
|
||||
const high = this.mem.getInt32(addr + 4, true);
|
||||
return low + high * 4294967296;
|
||||
}
|
||||
|
||||
const loadValue = (addr) => {
|
||||
const f = mem().getFloat64(addr, true);
|
||||
const f = this.mem.getFloat64(addr, true);
|
||||
if (f === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -138,7 +165,7 @@
|
|||
return f;
|
||||
}
|
||||
|
||||
const id = mem().getUint32(addr, true);
|
||||
const id = this.mem.getUint32(addr, true);
|
||||
return this._values[id];
|
||||
}
|
||||
|
||||
|
@ -147,57 +174,62 @@
|
|||
|
||||
if (typeof v === "number") {
|
||||
if (isNaN(v)) {
|
||||
mem().setUint32(addr + 4, nanHead, true);
|
||||
mem().setUint32(addr, 0, true);
|
||||
this.mem.setUint32(addr + 4, nanHead, true);
|
||||
this.mem.setUint32(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
if (v === 0) {
|
||||
mem().setUint32(addr + 4, nanHead, true);
|
||||
mem().setUint32(addr, 1, true);
|
||||
this.mem.setUint32(addr + 4, nanHead, true);
|
||||
this.mem.setUint32(addr, 1, true);
|
||||
return;
|
||||
}
|
||||
mem().setFloat64(addr, v, true);
|
||||
this.mem.setFloat64(addr, v, true);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (v) {
|
||||
case undefined:
|
||||
mem().setFloat64(addr, 0, true);
|
||||
this.mem.setFloat64(addr, 0, true);
|
||||
return;
|
||||
case null:
|
||||
mem().setUint32(addr + 4, nanHead, true);
|
||||
mem().setUint32(addr, 2, true);
|
||||
this.mem.setUint32(addr + 4, nanHead, true);
|
||||
this.mem.setUint32(addr, 2, true);
|
||||
return;
|
||||
case true:
|
||||
mem().setUint32(addr + 4, nanHead, true);
|
||||
mem().setUint32(addr, 3, true);
|
||||
this.mem.setUint32(addr + 4, nanHead, true);
|
||||
this.mem.setUint32(addr, 3, true);
|
||||
return;
|
||||
case false:
|
||||
mem().setUint32(addr + 4, nanHead, true);
|
||||
mem().setUint32(addr, 4, true);
|
||||
this.mem.setUint32(addr + 4, nanHead, true);
|
||||
this.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 id = this._ids.get(v);
|
||||
if (id === undefined) {
|
||||
id = this._idPool.pop();
|
||||
if (id === undefined) {
|
||||
id = this._values.length;
|
||||
}
|
||||
this._values[id] = v;
|
||||
this._goRefCounts[id] = 0;
|
||||
this._ids.set(v, id);
|
||||
}
|
||||
let typeFlag = 0;
|
||||
this._goRefCounts[id]++;
|
||||
let typeFlag = 1;
|
||||
switch (typeof v) {
|
||||
case "string":
|
||||
typeFlag = 1;
|
||||
break;
|
||||
case "symbol":
|
||||
typeFlag = 2;
|
||||
break;
|
||||
case "function":
|
||||
case "symbol":
|
||||
typeFlag = 3;
|
||||
break;
|
||||
case "function":
|
||||
typeFlag = 4;
|
||||
break;
|
||||
}
|
||||
mem().setUint32(addr + 4, nanHead | typeFlag, true);
|
||||
mem().setUint32(addr, ref, true);
|
||||
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
|
||||
this.mem.setUint32(addr, id, true);
|
||||
}
|
||||
|
||||
const loadSlice = (addr) => {
|
||||
|
@ -232,11 +264,13 @@
|
|||
|
||||
// func wasmExit(code int32)
|
||||
"runtime.wasmExit": (sp) => {
|
||||
const code = mem().getInt32(sp + 8, true);
|
||||
const code = this.mem.getInt32(sp + 8, true);
|
||||
this.exited = true;
|
||||
delete this._inst;
|
||||
delete this._values;
|
||||
delete this._refs;
|
||||
delete this._goRefCounts;
|
||||
delete this._ids;
|
||||
delete this._idPool;
|
||||
this.exit(code);
|
||||
},
|
||||
|
||||
|
@ -244,20 +278,25 @@
|
|||
"runtime.wasmWrite": (sp) => {
|
||||
const fd = getInt64(sp + 8);
|
||||
const p = getInt64(sp + 16);
|
||||
const n = mem().getInt32(sp + 24, true);
|
||||
const n = this.mem.getInt32(sp + 24, true);
|
||||
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
|
||||
},
|
||||
|
||||
// func nanotime() int64
|
||||
"runtime.nanotime": (sp) => {
|
||||
// func resetMemoryDataView()
|
||||
"runtime.resetMemoryDataView": (sp) => {
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
},
|
||||
|
||||
// func nanotime1() int64
|
||||
"runtime.nanotime1": (sp) => {
|
||||
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
|
||||
},
|
||||
|
||||
// func walltime() (sec int64, nsec int32)
|
||||
"runtime.walltime": (sp) => {
|
||||
// func walltime1() (sec int64, nsec int32)
|
||||
"runtime.walltime1": (sp) => {
|
||||
const msec = (new Date).getTime();
|
||||
setInt64(sp + 8, msec / 1000);
|
||||
mem().setInt32(sp + 16, (msec % 1000) * 1000000, true);
|
||||
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
|
||||
},
|
||||
|
||||
// func scheduleTimeoutEvent(delay int64) int32
|
||||
|
@ -276,12 +315,12 @@
|
|||
},
|
||||
getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
|
||||
));
|
||||
mem().setInt32(sp + 16, id, true);
|
||||
this.mem.setInt32(sp + 16, id, true);
|
||||
},
|
||||
|
||||
// func clearTimeoutEvent(id int32)
|
||||
"runtime.clearTimeoutEvent": (sp) => {
|
||||
const id = mem().getInt32(sp + 8, true);
|
||||
const id = this.mem.getInt32(sp + 8, true);
|
||||
clearTimeout(this._scheduledTimeouts.get(id));
|
||||
this._scheduledTimeouts.delete(id);
|
||||
},
|
||||
|
@ -291,6 +330,18 @@
|
|||
crypto.getRandomValues(loadSlice(sp + 8));
|
||||
},
|
||||
|
||||
// func finalizeRef(v ref)
|
||||
"syscall/js.finalizeRef": (sp) => {
|
||||
const id = this.mem.getUint32(sp + 8, true);
|
||||
this._goRefCounts[id]--;
|
||||
if (this._goRefCounts[id] === 0) {
|
||||
const v = this._values[id];
|
||||
this._values[id] = null;
|
||||
this._ids.delete(v);
|
||||
this._idPool.push(id);
|
||||
}
|
||||
},
|
||||
|
||||
// func stringVal(value string) ref
|
||||
"syscall/js.stringVal": (sp) => {
|
||||
storeValue(sp + 24, loadString(sp + 8));
|
||||
|
@ -308,6 +359,11 @@
|
|||
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
|
||||
},
|
||||
|
||||
// func valueDelete(v ref, p string)
|
||||
"syscall/js.valueDelete": (sp) => {
|
||||
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
|
||||
},
|
||||
|
||||
// func valueIndex(v ref, i int) ref
|
||||
"syscall/js.valueIndex": (sp) => {
|
||||
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
|
||||
|
@ -327,10 +383,10 @@
|
|||
const result = Reflect.apply(m, v, args);
|
||||
sp = this._inst.exports.getsp(); // see comment above
|
||||
storeValue(sp + 56, result);
|
||||
mem().setUint8(sp + 64, 1);
|
||||
this.mem.setUint8(sp + 64, 1);
|
||||
} catch (err) {
|
||||
storeValue(sp + 56, err);
|
||||
mem().setUint8(sp + 64, 0);
|
||||
this.mem.setUint8(sp + 64, 0);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -342,10 +398,10 @@
|
|||
const result = Reflect.apply(v, undefined, args);
|
||||
sp = this._inst.exports.getsp(); // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
mem().setUint8(sp + 48, 1);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
storeValue(sp + 40, err);
|
||||
mem().setUint8(sp + 48, 0);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -357,10 +413,10 @@
|
|||
const result = Reflect.construct(v, args);
|
||||
sp = this._inst.exports.getsp(); // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
mem().setUint8(sp + 48, 1);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
storeValue(sp + 40, err);
|
||||
mem().setUint8(sp + 48, 0);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -384,7 +440,7 @@
|
|||
|
||||
// func valueInstanceOf(v ref, t ref) bool
|
||||
"syscall/js.valueInstanceOf": (sp) => {
|
||||
mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16));
|
||||
this.mem.setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16));
|
||||
},
|
||||
|
||||
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||
|
@ -392,13 +448,13 @@
|
|||
const dst = loadSlice(sp + 8);
|
||||
const src = loadValue(sp + 32);
|
||||
if (!(src instanceof Uint8Array)) {
|
||||
mem().setUint8(sp + 48, 0);
|
||||
this.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);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
// func copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||
|
@ -406,13 +462,13 @@
|
|||
const dst = loadValue(sp + 8);
|
||||
const src = loadSlice(sp + 16);
|
||||
if (!(dst instanceof Uint8Array)) {
|
||||
mem().setUint8(sp + 48, 0);
|
||||
this.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);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
"debug": (value) => {
|
||||
|
@ -424,7 +480,8 @@
|
|||
|
||||
async run(instance) {
|
||||
this._inst = instance;
|
||||
this._values = [ // TODO: garbage collection
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
this._values = [ // JS values that Go currently has references to, indexed by reference id
|
||||
NaN,
|
||||
0,
|
||||
null,
|
||||
|
@ -433,10 +490,10 @@
|
|||
global,
|
||||
this,
|
||||
];
|
||||
this._refs = new Map();
|
||||
this.exited = false;
|
||||
|
||||
const mem = new DataView(this._inst.exports.mem.buffer)
|
||||
this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id
|
||||
this._ids = new Map(); // mapping from JS values to reference ids
|
||||
this._idPool = []; // unused ids that have been garbage collected
|
||||
this.exited = false; // whether the Go program has exited
|
||||
|
||||
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
|
||||
let offset = 4096;
|
||||
|
@ -444,7 +501,7 @@
|
|||
const strPtr = (str) => {
|
||||
const ptr = offset;
|
||||
const bytes = encoder.encode(str + "\0");
|
||||
new Uint8Array(mem.buffer, offset, bytes.length).set(bytes);
|
||||
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
|
||||
offset += bytes.length;
|
||||
if (offset % 8 !== 0) {
|
||||
offset += 8 - (offset % 8);
|
||||
|
@ -458,17 +515,18 @@
|
|||
this.argv.forEach((arg) => {
|
||||
argvPtrs.push(strPtr(arg));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const keys = Object.keys(this.env).sort();
|
||||
argvPtrs.push(keys.length);
|
||||
keys.forEach((key) => {
|
||||
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const argv = offset;
|
||||
argvPtrs.forEach((ptr) => {
|
||||
mem.setUint32(offset, ptr, true);
|
||||
mem.setUint32(offset + 4, 0, true);
|
||||
this.mem.setUint32(offset, ptr, true);
|
||||
this.mem.setUint32(offset + 4, 0, true);
|
||||
offset += 8;
|
||||
});
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ var (
|
|||
Height = 768
|
||||
|
||||
// Cascade offset for creating multiple windows.
|
||||
Cascade = render.NewPoint(10, 10)
|
||||
Cascade = render.NewPoint(10, 32)
|
||||
CascadeStep = render.NewPoint(24, 24)
|
||||
CascadeLoops = 1
|
||||
|
||||
|
@ -45,6 +45,20 @@ func main() {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
// Menu bar.
|
||||
menu := ui.NewMenuBar("Main Menu")
|
||||
file := menu.AddMenu("Options")
|
||||
file.AddItem("New window", func() {
|
||||
addWindow(mw)
|
||||
})
|
||||
file.AddItem("Close all windows", func() {
|
||||
OpenWindows -= mw.Supervisor().CloseAllWindows()
|
||||
})
|
||||
|
||||
menu.Supervise(mw.Supervisor())
|
||||
menu.Compute(mw.Engine)
|
||||
mw.Pack(menu, menu.PackTop())
|
||||
|
||||
// Add some windows to play with.
|
||||
addWindow(mw)
|
||||
addWindow(mw)
|
||||
|
@ -95,7 +109,7 @@ func addWindow(mw *ui.MainWindow) {
|
|||
Cascade.Add(CascadeStep)
|
||||
if Cascade.Y > Height-240-64 {
|
||||
CascadeLoops++
|
||||
Cascade.Y = 24
|
||||
Cascade.Y = 32
|
||||
Cascade.X = 24 * CascadeLoops
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ var (
|
|||
Height = 768
|
||||
|
||||
// Cascade offset for creating multiple windows.
|
||||
Cascade = render.NewPoint(10, 10)
|
||||
Cascade = render.NewPoint(10, 32)
|
||||
CascadeStep = render.NewPoint(24, 24)
|
||||
CascadeLoops = 1
|
||||
|
||||
|
@ -73,6 +73,20 @@ func main() {
|
|||
height-lbl.Size().H-20,
|
||||
))
|
||||
|
||||
// Menu bar.
|
||||
menu := ui.NewMenuBar("Main Menu")
|
||||
file := menu.AddMenu("Options")
|
||||
file.AddItem("New window", func() {
|
||||
addWindow(mw, frame, supervisor)
|
||||
})
|
||||
file.AddItem("Close all windows", func() {
|
||||
OpenWindows -= supervisor.CloseAllWindows()
|
||||
})
|
||||
|
||||
menu.Supervise(supervisor)
|
||||
menu.Compute(mw)
|
||||
frame.Pack(menu, menu.PackTop())
|
||||
|
||||
// Add some windows to play with.
|
||||
addWindow(mw, frame, supervisor)
|
||||
addWindow(mw, frame, supervisor)
|
||||
|
@ -85,6 +99,7 @@ func main() {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
frame.Present(mw, frame.Point())
|
||||
lbl.Present(mw, lbl.Point())
|
||||
supervisor.Loop(ev)
|
||||
supervisor.Present(mw)
|
||||
|
|
13
functions.go
13
functions.go
|
@ -41,6 +41,19 @@ func AbsoluteRect(w Widget) render.Rect {
|
|||
}
|
||||
}
|
||||
|
||||
// HasParent returns whether the target widget is a descendant of the parent.
|
||||
// This scans the parents of the widget recursively until it finds a match.
|
||||
func HasParent(w Widget, parent Widget) bool {
|
||||
next, ok := w.Parent()
|
||||
for ok {
|
||||
if next == parent {
|
||||
return true
|
||||
}
|
||||
next, ok = next.Parent()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// widgetInFocusedWindow returns whether a widget (like a Button) is a
|
||||
// descendant of a Window that is being Window Managed by Supervisor, and
|
||||
// said window is in a Focused state.
|
||||
|
|
220
menu.go
220
menu.go
|
@ -4,39 +4,78 @@ import (
|
|||
"fmt"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/ui/theme"
|
||||
)
|
||||
|
||||
// Menu is a rectangle that holds menu items.
|
||||
// MenuWidth sets the width of all popup menus. TODO, widths should be automatic.
|
||||
var MenuWidth = 180
|
||||
|
||||
// Menu is a frame that holds menu items. It is the
|
||||
type Menu struct {
|
||||
BaseWidget
|
||||
Name string
|
||||
|
||||
body *Frame
|
||||
supervisor *Supervisor
|
||||
body *Frame
|
||||
items []*MenuItem
|
||||
}
|
||||
|
||||
// NewMenu creates a new Menu. It is hidden by default. Usually you'll
|
||||
// use it with a MenuButton or in a right-click handler.
|
||||
func NewMenu(name string) *Menu {
|
||||
w := &Menu{
|
||||
Name: name,
|
||||
body: NewFrame(name + ":Body"),
|
||||
Name: name,
|
||||
body: NewFrame(name + ":Body"),
|
||||
items: []*MenuItem{},
|
||||
}
|
||||
w.body.Configure(Config{
|
||||
Width: 150,
|
||||
BorderSize: 12,
|
||||
Width: MenuWidth,
|
||||
Height: 100,
|
||||
BorderSize: 0,
|
||||
BorderStyle: BorderRaised,
|
||||
Background: render.Grey,
|
||||
Background: theme.ButtonBackgroundColor,
|
||||
})
|
||||
w.body.SetParent(w)
|
||||
w.IDFunc(func() string {
|
||||
return fmt.Sprintf("Menu<%s>", w.Name)
|
||||
})
|
||||
return w
|
||||
}
|
||||
|
||||
// Children returns the child frame of the menu.
|
||||
func (w *Menu) Children() []Widget {
|
||||
return []Widget{
|
||||
w.body,
|
||||
}
|
||||
}
|
||||
|
||||
// Supervise the Menu. This will add all current and future MenuItem widgets
|
||||
// to the supervisor.
|
||||
func (w *Menu) Supervise(s *Supervisor) {
|
||||
w.supervisor = s
|
||||
for _, item := range w.items {
|
||||
w.supervisor.Add(item)
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the menu
|
||||
func (w *Menu) Compute(e render.Engine) {
|
||||
w.body.Compute(e)
|
||||
|
||||
// TODO: ideally the Frame Pack Compute would fix the size of the body
|
||||
// for the height to match the height of the menu items... but for now
|
||||
// manually set the height.
|
||||
var maxWidth int
|
||||
var height int
|
||||
for _, child := range w.body.Children() {
|
||||
size := child.Size()
|
||||
if size.W > maxWidth {
|
||||
maxWidth = size.W
|
||||
}
|
||||
height += child.Size().H
|
||||
}
|
||||
w.body.Resize(render.NewRect(maxWidth, height))
|
||||
|
||||
// Call the BaseWidget Compute in case we have subscribers.
|
||||
w.BaseWidget.Compute(e)
|
||||
}
|
||||
|
@ -51,19 +90,69 @@ func (w *Menu) Present(e render.Engine, p render.Point) {
|
|||
|
||||
// AddItem quickly adds an item to a menu.
|
||||
func (w *Menu) AddItem(label string, command func()) *MenuItem {
|
||||
menu := NewMenuItem(label, command)
|
||||
menu := NewMenuItem(label, "", command)
|
||||
|
||||
// Add a Click handler that closes the menu when a selection is made.
|
||||
menu.Handle(Click, w.menuClickHandler)
|
||||
|
||||
w.Pack(menu)
|
||||
return menu
|
||||
}
|
||||
|
||||
// AddItemAccel quickly adds an item to a menu with a shortcut key label.
|
||||
func (w *Menu) AddItemAccel(label string, accelerator string, command func()) *MenuItem {
|
||||
menu := NewMenuItem(label, accelerator, command)
|
||||
|
||||
// Add a Click handler that closes the menu when a selection is made.
|
||||
menu.Handle(Click, w.menuClickHandler)
|
||||
|
||||
w.Pack(menu)
|
||||
return menu
|
||||
}
|
||||
|
||||
// Click handler for all menu items, to also close the menu behind them.
|
||||
func (w *Menu) menuClickHandler(ed EventData) error {
|
||||
if w.supervisor != nil {
|
||||
w.supervisor.PopModal(w)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddSeparator adds a separator bar to the menu to delineate items.
|
||||
func (w *Menu) AddSeparator() *MenuItem {
|
||||
sep := NewMenuSeparator()
|
||||
w.Pack(sep)
|
||||
return sep
|
||||
}
|
||||
|
||||
// Pack a menu item onto the menu.
|
||||
func (w *Menu) Pack(item *MenuItem) {
|
||||
w.items = append(w.items, item)
|
||||
w.body.Pack(item, Pack{
|
||||
Side: NE,
|
||||
// Expand: true,
|
||||
// Padding: 8,
|
||||
Side: N,
|
||||
FillX: true,
|
||||
})
|
||||
if w.supervisor != nil {
|
||||
w.supervisor.Add(item)
|
||||
}
|
||||
}
|
||||
|
||||
// Size returns the size of the menu's body.
|
||||
func (w *Menu) Size() render.Rect {
|
||||
return w.body.Size()
|
||||
}
|
||||
|
||||
// Rect returns the rect of the menu's body.
|
||||
func (w *Menu) Rect() render.Rect {
|
||||
// TODO: the height reports wrong (0), manually add up the MenuItem sizes.
|
||||
// This manifests in Supervisor.runWidgetEvents when checking if the cursor
|
||||
// clicked outside the rect of the active menu modal.
|
||||
rect := w.body.Rect()
|
||||
rect.H = 0
|
||||
for _, child := range w.body.Children() {
|
||||
rect.H += child.Size().H
|
||||
}
|
||||
return rect
|
||||
}
|
||||
|
||||
// MenuItem is an item in a Menu.
|
||||
|
@ -72,28 +161,71 @@ type MenuItem struct {
|
|||
Label string
|
||||
Accelerator string
|
||||
Command func()
|
||||
separator bool
|
||||
button *Button
|
||||
|
||||
// store of most recent bg color set on a menu item
|
||||
cacheBg render.Color
|
||||
cacheFg render.Color
|
||||
}
|
||||
|
||||
// NewMenuItem creates a new menu item.
|
||||
func NewMenuItem(label string, command func()) *MenuItem {
|
||||
func NewMenuItem(label, accelerator string, command func()) *MenuItem {
|
||||
w := &MenuItem{
|
||||
Label: label,
|
||||
Command: command,
|
||||
Label: label,
|
||||
Accelerator: accelerator,
|
||||
Command: command,
|
||||
}
|
||||
w.IDFunc(func() string {
|
||||
return fmt.Sprintf("MenuItem<%s>", w.Label)
|
||||
})
|
||||
|
||||
font := DefaultFont
|
||||
font.Color = render.White
|
||||
font.Color = render.Black
|
||||
font.PadX = 12
|
||||
w.Button.child = NewLabel(Label{
|
||||
Text: label,
|
||||
Font: font,
|
||||
font.PadY = 2
|
||||
|
||||
// The button child will be a Frame so we can have a left-aligned label
|
||||
// and a right-aligned accelerator.
|
||||
frame := NewFrame(label + ":Frame")
|
||||
frame.Configure(Config{
|
||||
Width: MenuWidth,
|
||||
})
|
||||
{
|
||||
// Left of frame: menu item label
|
||||
lbl := NewLabel(Label{
|
||||
Text: label,
|
||||
Font: font,
|
||||
})
|
||||
frame.Pack(lbl, Pack{
|
||||
Side: W,
|
||||
})
|
||||
|
||||
// On the right: accelerator shortcut key
|
||||
if accelerator != "" {
|
||||
accel := NewLabel(Label{
|
||||
Text: accelerator,
|
||||
Font: font,
|
||||
})
|
||||
frame.Pack(accel, Pack{
|
||||
Side: E,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
w.Button.child = frame
|
||||
w.Button.Configure(Config{
|
||||
Background: render.Blue,
|
||||
BorderSize: 0,
|
||||
Background: theme.ButtonBackgroundColor,
|
||||
})
|
||||
|
||||
w.Button.Handle(MouseOver, func(ed EventData) error {
|
||||
w.setHoverStyle(true)
|
||||
return nil
|
||||
})
|
||||
w.Button.Handle(MouseOut, func(ed EventData) error {
|
||||
w.setHoverStyle(false)
|
||||
return nil
|
||||
})
|
||||
|
||||
w.Button.Handle(Click, func(ed EventData) error {
|
||||
|
@ -104,3 +236,53 @@ func NewMenuItem(label string, command func()) *MenuItem {
|
|||
// Assign the button
|
||||
return w
|
||||
}
|
||||
|
||||
// NewMenuSeparator creates a separator menu item.
|
||||
func NewMenuSeparator() *MenuItem {
|
||||
w := &MenuItem{
|
||||
separator: true,
|
||||
}
|
||||
w.IDFunc(func() string {
|
||||
return "MenuItem<separator>"
|
||||
})
|
||||
w.Button.child = NewFrame("Menu Separator")
|
||||
w.Button.Configure(Config{
|
||||
Width: MenuWidth,
|
||||
Height: 2,
|
||||
BorderSize: 1,
|
||||
BorderStyle: BorderSunken,
|
||||
BorderColor: render.Grey,
|
||||
})
|
||||
return w
|
||||
}
|
||||
|
||||
// Set the hover styling (text/bg color)
|
||||
func (w *MenuItem) setHoverStyle(hovering bool) {
|
||||
// Note: this only works if the MenuItem is using the standard
|
||||
// Frame and Labels layout created by AddItem(). If not, this function
|
||||
// does nothing.
|
||||
|
||||
// BG color.
|
||||
if hovering {
|
||||
w.cacheBg = w.Background()
|
||||
w.SetBackground(render.SkyBlue)
|
||||
} else {
|
||||
w.SetBackground(w.cacheBg)
|
||||
}
|
||||
|
||||
frame, ok := w.Button.child.(*Frame)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for _, widget := range frame.Children() {
|
||||
if label, ok := widget.(*Label); ok {
|
||||
if hovering {
|
||||
w.cacheFg = label.Font.Color
|
||||
label.Font.Color = render.White
|
||||
} else {
|
||||
label.Font.Color = w.cacheFg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
80
menu_bar.go
Normal file
80
menu_bar.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/ui/theme"
|
||||
)
|
||||
|
||||
// MenuFont is the default font settings for MenuBar buttons.
|
||||
var MenuFont = render.Text{
|
||||
Size: 12,
|
||||
Color: render.Black,
|
||||
PadX: 4,
|
||||
PadY: 2,
|
||||
}
|
||||
|
||||
// MenuBar is a frame that holds several MenuButtons, such as for the main
|
||||
// menu at the top of a window.
|
||||
type MenuBar struct {
|
||||
Frame
|
||||
name string
|
||||
|
||||
supervisor *Supervisor
|
||||
buttons []*MenuButton
|
||||
}
|
||||
|
||||
// NewMenuBar creates a new menu bar frame.
|
||||
func NewMenuBar(name string) *MenuBar {
|
||||
w := &MenuBar{
|
||||
name: name,
|
||||
buttons: []*MenuButton{},
|
||||
}
|
||||
w.SetBackground(theme.ButtonBackgroundColor)
|
||||
w.Frame.Setup()
|
||||
w.IDFunc(func() string {
|
||||
return fmt.Sprintf("MenuBar<%s>", w.name)
|
||||
})
|
||||
return w
|
||||
}
|
||||
|
||||
// Supervise the menu bar, making its child menu buttons work correctly.
|
||||
func (w *MenuBar) Supervise(s *Supervisor) {
|
||||
w.supervisor = s
|
||||
|
||||
// Supervise the existing buttons.
|
||||
for _, btn := range w.buttons {
|
||||
s.Add(btn)
|
||||
btn.Supervise(s)
|
||||
}
|
||||
}
|
||||
|
||||
// AddMenu adds a new menu button to the bar. Returns the MenuButton
|
||||
// object so that you can add items to it.
|
||||
func (w *MenuBar) AddMenu(label string) *MenuButton {
|
||||
btn := NewMenuButton(label, NewLabel(Label{
|
||||
Text: label,
|
||||
Font: MenuFont,
|
||||
}))
|
||||
w.buttons = append(w.buttons, btn)
|
||||
|
||||
// Pack and supervise it.
|
||||
w.Pack(btn, Pack{
|
||||
Side: W,
|
||||
})
|
||||
if w.supervisor != nil {
|
||||
w.supervisor.Add(btn)
|
||||
btn.Supervise(w.supervisor)
|
||||
}
|
||||
return btn
|
||||
}
|
||||
|
||||
// PackTop returns the default Frame Pack settings to place the menu
|
||||
// at the top of the parent widget.
|
||||
func (w *MenuBar) PackTop() Pack {
|
||||
return Pack{
|
||||
Side: N,
|
||||
FillX: true,
|
||||
}
|
||||
}
|
195
menu_button.go
Normal file
195
menu_button.go
Normal file
|
@ -0,0 +1,195 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/ui/theme"
|
||||
)
|
||||
|
||||
// MenuButton is a button that opens a menu when clicked.
|
||||
//
|
||||
// After creating a MenuButton, call AddItem() to add options and callback
|
||||
// functions to fill out the menu. When the MenuButton is clicked, its menu
|
||||
// will be drawn and take modal priority in the Supervisor.
|
||||
type MenuButton struct {
|
||||
Button
|
||||
|
||||
name string
|
||||
supervisor *Supervisor
|
||||
menu *Menu
|
||||
}
|
||||
|
||||
// NewMenuButton creates a new MenuButton (labels recommended).
|
||||
//
|
||||
// If the child is a Label, this function will set some sensible padding on
|
||||
// its font if the Label does not already have non-zero padding set.
|
||||
func NewMenuButton(name string, child Widget) *MenuButton {
|
||||
w := &MenuButton{
|
||||
name: name,
|
||||
}
|
||||
w.Button.child = child
|
||||
|
||||
// If it's a Label (most common), set sensible default padding.
|
||||
if label, ok := child.(*Label); ok {
|
||||
if label.Font.Padding == 0 && label.Font.PadX == 0 && label.Font.PadY == 0 {
|
||||
label.Font.PadX = 8
|
||||
label.Font.PadY = 4
|
||||
}
|
||||
}
|
||||
|
||||
w.IDFunc(func() string {
|
||||
return fmt.Sprintf("MenuButton<%s>", name)
|
||||
})
|
||||
|
||||
w.setup()
|
||||
return w
|
||||
}
|
||||
|
||||
// Supervise the MenuButton. This is necessary for the pop-up menu to work
|
||||
// when the button is clicked.
|
||||
func (w *MenuButton) Supervise(s *Supervisor) {
|
||||
w.initMenu()
|
||||
w.supervisor = s
|
||||
w.menu.Supervise(s)
|
||||
}
|
||||
|
||||
// AddItem adds a new option to the MenuButton's menu.
|
||||
func (w *MenuButton) AddItem(label string, f func()) {
|
||||
w.initMenu()
|
||||
w.menu.AddItem(label, f)
|
||||
}
|
||||
|
||||
// AddItemAccel adds a new menu option with hotkey text.
|
||||
func (w *MenuButton) AddItemAccel(label string, accelerator string, f func()) *MenuItem {
|
||||
w.initMenu()
|
||||
return w.menu.AddItemAccel(label, accelerator, f)
|
||||
}
|
||||
|
||||
// AddSeparator adds a separator to the menu.
|
||||
func (w *MenuButton) AddSeparator() {
|
||||
w.initMenu()
|
||||
w.menu.AddSeparator()
|
||||
}
|
||||
|
||||
// Compute to re-evaluate the button state (in the case of radio buttons where
|
||||
// a different button will affect the state of this one when clicked).
|
||||
func (w *MenuButton) Compute(e render.Engine) {
|
||||
if w.menu != nil {
|
||||
w.menu.Compute(e)
|
||||
w.positionMenu(e)
|
||||
}
|
||||
}
|
||||
|
||||
// positionMenu sets the position where the pop-up menu will appear when
|
||||
// the button is clicked. Usually, the menu appears below and to the right of
|
||||
// the button. But if the menu will hit a window boundary, its position will
|
||||
// be adjusted to fit the window while trying not to overlap its own button.
|
||||
func (w *MenuButton) positionMenu(e render.Engine) {
|
||||
var (
|
||||
// Position and size of the MenuButton button.
|
||||
buttonPoint = w.Point()
|
||||
buttonSize = w.Size()
|
||||
|
||||
// Size of the actual desktop window.
|
||||
Width, Height = e.WindowSize()
|
||||
)
|
||||
|
||||
// Ideal location: below and to the right of the button.
|
||||
w.menu.MoveTo(render.Point{
|
||||
X: buttonPoint.X,
|
||||
Y: buttonPoint.Y + buttonSize.H + w.BoxThickness(2),
|
||||
})
|
||||
|
||||
var (
|
||||
// Size of the menu.
|
||||
menuPoint = w.menu.Point()
|
||||
menuSize = w.menu.Rect()
|
||||
margin = 8 // keep away from directly touching window edges
|
||||
topMargin = 32 // keep room for standard Menu Bar
|
||||
)
|
||||
|
||||
// Will we clip out the bottom of the window?
|
||||
if menuPoint.Y+menuSize.H+margin > Height {
|
||||
// Put us above the button instead, with the bottom of the
|
||||
// menu touching the top of the button.
|
||||
menuPoint = render.Point{
|
||||
X: buttonPoint.X,
|
||||
Y: buttonPoint.Y - menuSize.H - w.BoxThickness(2),
|
||||
}
|
||||
|
||||
// If this would put us over the TOP edge of the window now,
|
||||
// cap the movement so the top of the menu is visible. We can't
|
||||
// avoid overlapping the button with the menu so might as well
|
||||
// start now.
|
||||
if menuPoint.Y < topMargin {
|
||||
menuPoint.Y = topMargin
|
||||
}
|
||||
|
||||
w.menu.MoveTo(menuPoint)
|
||||
}
|
||||
|
||||
// Will we clip out the right of the window?
|
||||
if menuPoint.X+menuSize.W > Width {
|
||||
// Move us in from the right side of the window.
|
||||
var delta = Width - menuSize.W - margin
|
||||
w.menu.MoveTo(render.Point{
|
||||
X: delta,
|
||||
Y: menuPoint.Y,
|
||||
})
|
||||
}
|
||||
_ = Width
|
||||
}
|
||||
|
||||
// setup the common things between checkboxes and radioboxes.
|
||||
func (w *MenuButton) setup() {
|
||||
w.Configure(Config{
|
||||
BorderSize: 1,
|
||||
BorderStyle: BorderSolid,
|
||||
Background: theme.ButtonBackgroundColor,
|
||||
})
|
||||
|
||||
w.Handle(MouseOver, func(ed EventData) error {
|
||||
w.hovering = true
|
||||
w.SetBorderStyle(BorderRaised)
|
||||
return nil
|
||||
})
|
||||
w.Handle(MouseOut, func(ed EventData) error {
|
||||
w.hovering = false
|
||||
w.SetBorderStyle(BorderSolid)
|
||||
return nil
|
||||
})
|
||||
|
||||
w.Handle(MouseDown, func(ed EventData) error {
|
||||
w.clicked = true
|
||||
w.SetBorderStyle(BorderSunken)
|
||||
return nil
|
||||
})
|
||||
w.Handle(MouseUp, func(ed EventData) error {
|
||||
w.clicked = false
|
||||
return nil
|
||||
})
|
||||
|
||||
w.Handle(Click, func(ed EventData) error {
|
||||
// Are we properly configured?
|
||||
if w.supervisor != nil && w.menu != nil {
|
||||
w.menu.Show()
|
||||
w.supervisor.PushModal(w.menu)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// initialize the Menu widget.
|
||||
func (w *MenuButton) initMenu() {
|
||||
if w.menu == nil {
|
||||
w.menu = NewMenu(w.name + ":Menu")
|
||||
w.menu.Hide()
|
||||
|
||||
// Handle closing the menu when clicked outside.
|
||||
w.menu.Handle(CloseModal, func(ed EventData) error {
|
||||
ed.Supervisor.PopModal(w.menu)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
83
menu_test.go
Normal file
83
menu_test.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package ui_test
|
||||
|
||||
import (
|
||||
"git.kirsle.net/go/ui"
|
||||
)
|
||||
|
||||
// Example of using the menu widgets.
|
||||
func ExampleMenu() {
|
||||
mw, err := ui.NewMainWindow("Menu Bar Example", 800, 600)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create a main menu for your window.
|
||||
menu := ui.NewMenuBar("Main Menu")
|
||||
|
||||
// File menu. Some items with accelerators, some without.
|
||||
// NOTE: key bindings are up to you, the accelerators are
|
||||
// purely decorative.
|
||||
file := menu.AddMenu("File")
|
||||
file.AddItemAccel("New", "Ctrl-N", func() {})
|
||||
file.AddItemAccel("Open", "Ctrl-O", func() {})
|
||||
file.AddItemAccel("Save", "Ctrl-S", func() {})
|
||||
file.AddItem("Save as...", func() {})
|
||||
file.AddSeparator()
|
||||
file.AddItem("Close window", func() {})
|
||||
file.AddItemAccel("Exit", "Alt-F4", func() {})
|
||||
|
||||
// Help menu.
|
||||
help := menu.AddMenu("Help")
|
||||
help.AddItemAccel("Contents", "F1", func() {})
|
||||
help.AddItem("About", func() {})
|
||||
|
||||
// Give the menu bar your Supervisor so it can wire all
|
||||
// events up and make the menus work.
|
||||
menu.Supervise(mw.Supervisor())
|
||||
|
||||
// Compute and pack the menu bar against the top of
|
||||
// the main window (or other parent container)
|
||||
menu.Compute(mw.Engine)
|
||||
mw.Pack(menu, menu.PackTop()) // Side: N, FillX: true
|
||||
|
||||
// Each loop you must then:
|
||||
// - Call Supervisor.Loop() as normal to handle events.
|
||||
// - Call Supervisor.Present() to draw the modal popup menus.
|
||||
// MainLoop() of the MainWindow does this for you.
|
||||
mw.MainLoop()
|
||||
}
|
||||
|
||||
// Example of using the MenuButton.
|
||||
func ExampleMenuButton() {
|
||||
mw, err := ui.NewMainWindow("Menu Button", 800, 600)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create a MenuButton much as you would a normal Button.
|
||||
btn := ui.NewMenuButton("Button1", ui.NewLabel(ui.Label{
|
||||
Text: "File",
|
||||
}))
|
||||
mw.Place(btn, ui.Place{ // place it in the center
|
||||
Center: true,
|
||||
Middle: true,
|
||||
})
|
||||
|
||||
// Add menu items to it.
|
||||
btn.AddItemAccel("New", "Ctrl-N", func() {})
|
||||
btn.AddItemAccel("Open", "Ctrl-O", func() {})
|
||||
btn.AddItemAccel("Save", "Ctrl-S", func() {})
|
||||
btn.AddItem("Save as...", func() {})
|
||||
btn.AddSeparator()
|
||||
btn.AddItem("Close window", func() {})
|
||||
btn.AddItemAccel("Exit", "Alt-F4", func() {})
|
||||
|
||||
// Add the button to Supervisor for events to work.
|
||||
btn.Supervise(mw.Supervisor())
|
||||
|
||||
// Each loop you must then:
|
||||
// - Call Supervisor.Loop() as normal to handle events.
|
||||
// - Call Supervisor.Present() to draw the modal popup menus.
|
||||
// MainLoop() of the MainWindow does this for you.
|
||||
mw.MainLoop()
|
||||
}
|
117
supervisor.go
117
supervisor.go
|
@ -32,6 +32,7 @@ const (
|
|||
CloseWindow
|
||||
MaximizeWindow
|
||||
MinimizeWindow
|
||||
CloseModal
|
||||
|
||||
// Lifecycle event handlers.
|
||||
Compute // fired whenever the widget runs Compute
|
||||
|
@ -45,6 +46,9 @@ type EventData struct {
|
|||
|
||||
// Engine is the render engine on Compute and Present events.
|
||||
Engine render.Engine
|
||||
|
||||
// Supervisor is the reference to the supervisor who sent the event.
|
||||
Supervisor *Supervisor
|
||||
}
|
||||
|
||||
// Supervisor keeps track of widgets of interest to notify them about
|
||||
|
@ -58,6 +62,9 @@ type Supervisor struct {
|
|||
clicked map[int]bool // map of widgets being clicked
|
||||
dd *DragDrop
|
||||
|
||||
// Stack of modal widgets that have event priority.
|
||||
modals []Widget
|
||||
|
||||
// List of window focus history for Window Manager.
|
||||
winFocus *FocusedWindow
|
||||
winTop *FocusedWindow // pointer to top-most window
|
||||
|
@ -76,6 +83,7 @@ func NewSupervisor() *Supervisor {
|
|||
widgets: map[int]WidgetSlot{},
|
||||
hovering: map[int]interface{}{},
|
||||
clicked: map[int]bool{},
|
||||
modals: []Widget{},
|
||||
dd: NewDragDrop(),
|
||||
}
|
||||
}
|
||||
|
@ -169,14 +177,18 @@ func (s *Supervisor) Loop(ev *event.State) error {
|
|||
// 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)
|
||||
handled, err := s.runWidgetEvents(XY, ev, hovering, outside, true)
|
||||
if err == ErrStopPropagation || handled {
|
||||
// A widget in the active window has accepted an event. Do not pass
|
||||
// the event also to lower widgets.
|
||||
return err
|
||||
// Only run if there is no active modal (modals have top priority)
|
||||
if len(s.modals) == 0 {
|
||||
handled, err := s.runWidgetEvents(XY, ev, hovering, outside, true)
|
||||
if err == ErrStopPropagation || handled {
|
||||
// A widget in the active window has accepted an event. Do not pass
|
||||
// the event also to lower widgets.
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Run events for the other widgets not in a managed window.
|
||||
// (Modal event priority is handled in runWidgetEvents)
|
||||
s.runWidgetEvents(XY, ev, hovering, outside, false)
|
||||
|
||||
return nil
|
||||
|
@ -233,13 +245,21 @@ func (s *Supervisor) Hovering(cursor render.Point) (hovering, outside []WidgetSl
|
|||
// 0: widgets NOT part of a managed window. On this pass, if a widget IS
|
||||
// a part of a window, it gets no events triggered.
|
||||
// 1: widgets are part of the active focused window.
|
||||
func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State, hovering, outside []WidgetSlot, toFocusedWindow bool) (bool, error) {
|
||||
func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State,
|
||||
hovering, outside []WidgetSlot, toFocusedWindow bool) (bool, error) {
|
||||
// Do we run any events?
|
||||
var (
|
||||
stopPropagation bool
|
||||
ranEvents bool
|
||||
)
|
||||
|
||||
// Do we have active modals? Modal widgets have top event priority given
|
||||
// only to the top-most modal.
|
||||
var modal Widget
|
||||
if len(s.modals) > 0 {
|
||||
modal = s.modals[len(s.modals)-1]
|
||||
}
|
||||
|
||||
// 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"
|
||||
|
@ -273,7 +293,8 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State, hovering,
|
|||
// 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 {
|
||||
// Does not apply when a modal widget is active.
|
||||
if cursorInsideFocusedWindow && modal == nil {
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -287,6 +308,14 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State, hovering,
|
|||
continue
|
||||
}
|
||||
|
||||
// If we have a modal active, validate this widget is a child of
|
||||
// the modal widget.
|
||||
if modal != nil {
|
||||
if !HasParent(w, modal) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the widget is part of a Window managed by Supervisor.
|
||||
isManaged, isFocused := widgetInFocusedWindow(w)
|
||||
|
||||
|
@ -344,6 +373,14 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State, hovering,
|
|||
w = child.widget
|
||||
)
|
||||
|
||||
// If we have a modal active, validate this widget is a child of
|
||||
// the modal widget.
|
||||
if modal != nil {
|
||||
if !HasParent(w, modal) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor is not intersecting the widget.
|
||||
if _, ok := s.hovering[id]; ok {
|
||||
handle(w.Event(MouseOut, EventData{
|
||||
|
@ -360,6 +397,16 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State, hovering,
|
|||
}
|
||||
}
|
||||
|
||||
// If a modal is active and a click was registered outside the modal's
|
||||
// bounding box, send the CloseModal event.
|
||||
if modal != nil && !XY.Inside(AbsoluteRect(modal)) {
|
||||
if ev.Button1 {
|
||||
modal.Event(CloseModal, EventData{
|
||||
Supervisor: s,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// If a stopPropagation was called, return it up the stack.
|
||||
if stopPropagation {
|
||||
return ranEvents, ErrStopPropagation
|
||||
|
@ -397,11 +444,28 @@ func (s *Supervisor) Present(e render.Engine) {
|
|||
|
||||
// Render the window manager windows from bottom to top.
|
||||
s.presentWindows(e)
|
||||
|
||||
// Render the modals from bottom to top.
|
||||
if len(s.modals) > 0 {
|
||||
for _, modal := range s.modals {
|
||||
modal.Present(e, modal.Point())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a widget to be supervised.
|
||||
// Add a widget to be supervised. Has no effect if the widget is already
|
||||
// under the supervisor's care.
|
||||
func (s *Supervisor) Add(w Widget) {
|
||||
s.lock.Lock()
|
||||
|
||||
// Check it's not already there.
|
||||
for _, child := range s.widgets {
|
||||
if child.widget == w {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Add it.
|
||||
s.widgets[s.serial] = WidgetSlot{
|
||||
id: s.serial,
|
||||
widget: w,
|
||||
|
@ -409,3 +473,40 @@ func (s *Supervisor) Add(w Widget) {
|
|||
s.serial++
|
||||
s.lock.Unlock()
|
||||
}
|
||||
|
||||
// PushModal sets the widget to be a "modal" for the Supervisor.
|
||||
//
|
||||
// Modal widgets have top-most event priority: mouse and click events go ONLY
|
||||
// to the modal and its descendants. Modals work as a stack: the most recently
|
||||
// pushed widget is the active modal, and popping the modal will make the
|
||||
// next most-recent widget be the active modal.
|
||||
//
|
||||
// If a Click event registers OUTSIDE the bounds of the modal widget, the
|
||||
// widget receives a CloseModal event.
|
||||
//
|
||||
// Returns the length of the modal stack.
|
||||
func (s *Supervisor) PushModal(w Widget) int {
|
||||
s.modals = append(s.modals, w)
|
||||
return len(s.modals)
|
||||
}
|
||||
|
||||
// PopModal attempts to pop the modal from the stack, but only if the modal
|
||||
// is at the top of the stack.
|
||||
//
|
||||
// A widget may safely attempt to PopModal itself on a CloseModal event to
|
||||
// close themselves when the user clicks outside their box. If there were a
|
||||
// newer modal on the stack, this PopModal action would do nothing.
|
||||
func (s *Supervisor) PopModal(w Widget) bool {
|
||||
// only can pop if the topmost widget is the one being asked for
|
||||
if len(s.modals) > 0 && s.modals[len(s.modals)-1] == w {
|
||||
modal := s.modals[len(s.modals)-1]
|
||||
modal.Hide()
|
||||
|
||||
// pop it off
|
||||
s.modals = s.modals[:len(s.modals)-1]
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -309,8 +309,13 @@ func (w *BaseWidget) Hidden() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
if parent, ok := w.Parent(); ok {
|
||||
return parent.Hidden()
|
||||
// Return if any parents are hidden.
|
||||
parent, ok := w.Parent()
|
||||
for ok {
|
||||
if parent.Hidden() {
|
||||
return true
|
||||
}
|
||||
parent, ok = parent.Parent()
|
||||
}
|
||||
|
||||
return false
|
||||
|
|
|
@ -296,6 +296,12 @@ func (w *Window) SetMaximized(v bool) {
|
|||
}
|
||||
}
|
||||
|
||||
// Close the window, hiding it from display and calling its CloseWindow handler.
|
||||
func (w *Window) Close() {
|
||||
w.Hide()
|
||||
w.Event(CloseWindow, EventData{})
|
||||
}
|
||||
|
||||
// Children returns the window's child widgets.
|
||||
func (w *Window) Children() []Widget {
|
||||
return []Widget{
|
||||
|
|
|
@ -164,6 +164,21 @@ func (s *Supervisor) IsPointInWindow(point render.Point) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// CloseAllWindows closes all open windows being managed by supervisor.
|
||||
// Returns the number of windows closed.
|
||||
func (s *Supervisor) CloseAllWindows() int {
|
||||
var (
|
||||
node = s.winFocus
|
||||
i = 0
|
||||
)
|
||||
for node != nil {
|
||||
i++
|
||||
node.window.Hide()
|
||||
node = node.next
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// presentWindows draws the windows from bottom to top.
|
||||
func (s *Supervisor) presentWindows(e render.Engine) {
|
||||
item := s.winBottom
|
||||
|
|
Loading…
Reference in New Issue
Block a user