Menus and Menu Bars #2
18
README.md
18
README.md
|
@ -138,25 +138,21 @@ most complex.
|
||||||
(drag it by its title bar, Close button, window focus, multiple overlapping
|
(drag it by its title bar, Close button, window focus, multiple overlapping
|
||||||
windows, and so on).
|
windows, and so on).
|
||||||
* [x] **Tooltip**: a mouse hover label attached to a widget.
|
* [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:**
|
**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
|
* [ ] **Scrollbar**: a Frame including a trough, scroll buttons and a
|
||||||
draggable slider.
|
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:**
|
**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
|
* [ ] **TextBox:** an editable text field that the user can focus and type
|
||||||
a value into.
|
a value into.
|
||||||
* Would depend on the WindowManager to manage focus for the widgets.
|
* Would depend on the WindowManager to manage focus for the widgets.
|
||||||
|
|
8
debug.go
8
debug.go
|
@ -5,6 +5,14 @@ import (
|
||||||
"strings"
|
"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
|
// WidgetTree returns a string representing the tree of widgets starting
|
||||||
// at a given widget.
|
// at a given widget.
|
||||||
func WidgetTree(root Widget) []string {
|
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");
|
global.fs = require("fs");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const enosys = () => {
|
||||||
|
const err = new Error("not implemented");
|
||||||
|
err.code = "ENOSYS";
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
if (!global.fs) {
|
if (!global.fs) {
|
||||||
let outputBuf = "";
|
let outputBuf = "";
|
||||||
global.fs = {
|
global.fs = {
|
||||||
|
@ -45,27 +51,53 @@
|
||||||
},
|
},
|
||||||
write(fd, buf, offset, length, position, callback) {
|
write(fd, buf, offset, length, position, callback) {
|
||||||
if (offset !== 0 || length !== buf.length || position !== null) {
|
if (offset !== 0 || length !== buf.length || position !== null) {
|
||||||
throw new Error("not implemented");
|
callback(enosys());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const n = this.writeSync(fd, buf);
|
const n = this.writeSync(fd, buf);
|
||||||
callback(null, n);
|
callback(null, n);
|
||||||
},
|
},
|
||||||
open(path, flags, mode, callback) {
|
chmod(path, mode, callback) { callback(enosys()); },
|
||||||
const err = new Error("not implemented");
|
chown(path, uid, gid, callback) { callback(enosys()); },
|
||||||
err.code = "ENOSYS";
|
close(fd, callback) { callback(enosys()); },
|
||||||
callback(err);
|
fchmod(fd, mode, callback) { callback(enosys()); },
|
||||||
},
|
fchown(fd, uid, gid, callback) { callback(enosys()); },
|
||||||
read(fd, buffer, offset, length, position, callback) {
|
fstat(fd, callback) { callback(enosys()); },
|
||||||
const err = new Error("not implemented");
|
fsync(fd, callback) { callback(null); },
|
||||||
err.code = "ENOSYS";
|
ftruncate(fd, length, callback) { callback(enosys()); },
|
||||||
callback(err);
|
lchown(path, uid, gid, callback) { callback(enosys()); },
|
||||||
},
|
link(path, link, callback) { callback(enosys()); },
|
||||||
fsync(fd, callback) {
|
lstat(path, callback) { callback(enosys()); },
|
||||||
callback(null);
|
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) {
|
if (!global.crypto) {
|
||||||
const nodeCrypto = require("crypto");
|
const nodeCrypto = require("crypto");
|
||||||
global.crypto = {
|
global.crypto = {
|
||||||
|
@ -113,24 +145,19 @@
|
||||||
this._scheduledTimeouts = new Map();
|
this._scheduledTimeouts = new Map();
|
||||||
this._nextCallbackTimeoutID = 1;
|
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) => {
|
const setInt64 = (addr, v) => {
|
||||||
mem().setUint32(addr + 0, v, true);
|
this.mem.setUint32(addr + 0, v, true);
|
||||||
mem().setUint32(addr + 4, Math.floor(v / 4294967296), true);
|
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInt64 = (addr) => {
|
const getInt64 = (addr) => {
|
||||||
const low = mem().getUint32(addr + 0, true);
|
const low = this.mem.getUint32(addr + 0, true);
|
||||||
const high = mem().getInt32(addr + 4, true);
|
const high = this.mem.getInt32(addr + 4, true);
|
||||||
return low + high * 4294967296;
|
return low + high * 4294967296;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadValue = (addr) => {
|
const loadValue = (addr) => {
|
||||||
const f = mem().getFloat64(addr, true);
|
const f = this.mem.getFloat64(addr, true);
|
||||||
if (f === 0) {
|
if (f === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -138,7 +165,7 @@
|
||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = mem().getUint32(addr, true);
|
const id = this.mem.getUint32(addr, true);
|
||||||
return this._values[id];
|
return this._values[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,57 +174,62 @@
|
||||||
|
|
||||||
if (typeof v === "number") {
|
if (typeof v === "number") {
|
||||||
if (isNaN(v)) {
|
if (isNaN(v)) {
|
||||||
mem().setUint32(addr + 4, nanHead, true);
|
this.mem.setUint32(addr + 4, nanHead, true);
|
||||||
mem().setUint32(addr, 0, true);
|
this.mem.setUint32(addr, 0, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (v === 0) {
|
if (v === 0) {
|
||||||
mem().setUint32(addr + 4, nanHead, true);
|
this.mem.setUint32(addr + 4, nanHead, true);
|
||||||
mem().setUint32(addr, 1, true);
|
this.mem.setUint32(addr, 1, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mem().setFloat64(addr, v, true);
|
this.mem.setFloat64(addr, v, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (v) {
|
switch (v) {
|
||||||
case undefined:
|
case undefined:
|
||||||
mem().setFloat64(addr, 0, true);
|
this.mem.setFloat64(addr, 0, true);
|
||||||
return;
|
return;
|
||||||
case null:
|
case null:
|
||||||
mem().setUint32(addr + 4, nanHead, true);
|
this.mem.setUint32(addr + 4, nanHead, true);
|
||||||
mem().setUint32(addr, 2, true);
|
this.mem.setUint32(addr, 2, true);
|
||||||
return;
|
return;
|
||||||
case true:
|
case true:
|
||||||
mem().setUint32(addr + 4, nanHead, true);
|
this.mem.setUint32(addr + 4, nanHead, true);
|
||||||
mem().setUint32(addr, 3, true);
|
this.mem.setUint32(addr, 3, true);
|
||||||
return;
|
return;
|
||||||
case false:
|
case false:
|
||||||
mem().setUint32(addr + 4, nanHead, true);
|
this.mem.setUint32(addr + 4, nanHead, true);
|
||||||
mem().setUint32(addr, 4, true);
|
this.mem.setUint32(addr, 4, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ref = this._refs.get(v);
|
let id = this._ids.get(v);
|
||||||
if (ref === undefined) {
|
if (id === undefined) {
|
||||||
ref = this._values.length;
|
id = this._idPool.pop();
|
||||||
this._values.push(v);
|
if (id === undefined) {
|
||||||
this._refs.set(v, ref);
|
id = this._values.length;
|
||||||
}
|
}
|
||||||
let typeFlag = 0;
|
this._values[id] = v;
|
||||||
|
this._goRefCounts[id] = 0;
|
||||||
|
this._ids.set(v, id);
|
||||||
|
}
|
||||||
|
this._goRefCounts[id]++;
|
||||||
|
let typeFlag = 1;
|
||||||
switch (typeof v) {
|
switch (typeof v) {
|
||||||
case "string":
|
case "string":
|
||||||
typeFlag = 1;
|
|
||||||
break;
|
|
||||||
case "symbol":
|
|
||||||
typeFlag = 2;
|
typeFlag = 2;
|
||||||
break;
|
break;
|
||||||
case "function":
|
case "symbol":
|
||||||
typeFlag = 3;
|
typeFlag = 3;
|
||||||
break;
|
break;
|
||||||
|
case "function":
|
||||||
|
typeFlag = 4;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
mem().setUint32(addr + 4, nanHead | typeFlag, true);
|
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
|
||||||
mem().setUint32(addr, ref, true);
|
this.mem.setUint32(addr, id, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadSlice = (addr) => {
|
const loadSlice = (addr) => {
|
||||||
|
@ -232,11 +264,13 @@
|
||||||
|
|
||||||
// func wasmExit(code int32)
|
// func wasmExit(code int32)
|
||||||
"runtime.wasmExit": (sp) => {
|
"runtime.wasmExit": (sp) => {
|
||||||
const code = mem().getInt32(sp + 8, true);
|
const code = this.mem.getInt32(sp + 8, true);
|
||||||
this.exited = true;
|
this.exited = true;
|
||||||
delete this._inst;
|
delete this._inst;
|
||||||
delete this._values;
|
delete this._values;
|
||||||
delete this._refs;
|
delete this._goRefCounts;
|
||||||
|
delete this._ids;
|
||||||
|
delete this._idPool;
|
||||||
this.exit(code);
|
this.exit(code);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -244,20 +278,25 @@
|
||||||
"runtime.wasmWrite": (sp) => {
|
"runtime.wasmWrite": (sp) => {
|
||||||
const fd = getInt64(sp + 8);
|
const fd = getInt64(sp + 8);
|
||||||
const p = getInt64(sp + 16);
|
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));
|
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
|
||||||
},
|
},
|
||||||
|
|
||||||
// func nanotime() int64
|
// func resetMemoryDataView()
|
||||||
"runtime.nanotime": (sp) => {
|
"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);
|
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
|
||||||
},
|
},
|
||||||
|
|
||||||
// func walltime() (sec int64, nsec int32)
|
// func walltime1() (sec int64, nsec int32)
|
||||||
"runtime.walltime": (sp) => {
|
"runtime.walltime1": (sp) => {
|
||||||
const msec = (new Date).getTime();
|
const msec = (new Date).getTime();
|
||||||
setInt64(sp + 8, msec / 1000);
|
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
|
// func scheduleTimeoutEvent(delay int64) int32
|
||||||
|
@ -276,12 +315,12 @@
|
||||||
},
|
},
|
||||||
getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
|
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)
|
// func clearTimeoutEvent(id int32)
|
||||||
"runtime.clearTimeoutEvent": (sp) => {
|
"runtime.clearTimeoutEvent": (sp) => {
|
||||||
const id = mem().getInt32(sp + 8, true);
|
const id = this.mem.getInt32(sp + 8, true);
|
||||||
clearTimeout(this._scheduledTimeouts.get(id));
|
clearTimeout(this._scheduledTimeouts.get(id));
|
||||||
this._scheduledTimeouts.delete(id);
|
this._scheduledTimeouts.delete(id);
|
||||||
},
|
},
|
||||||
|
@ -291,6 +330,18 @@
|
||||||
crypto.getRandomValues(loadSlice(sp + 8));
|
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
|
// func stringVal(value string) ref
|
||||||
"syscall/js.stringVal": (sp) => {
|
"syscall/js.stringVal": (sp) => {
|
||||||
storeValue(sp + 24, loadString(sp + 8));
|
storeValue(sp + 24, loadString(sp + 8));
|
||||||
|
@ -308,6 +359,11 @@
|
||||||
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
|
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
|
// func valueIndex(v ref, i int) ref
|
||||||
"syscall/js.valueIndex": (sp) => {
|
"syscall/js.valueIndex": (sp) => {
|
||||||
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
|
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
|
||||||
|
@ -327,10 +383,10 @@
|
||||||
const result = Reflect.apply(m, v, args);
|
const result = Reflect.apply(m, v, args);
|
||||||
sp = this._inst.exports.getsp(); // see comment above
|
sp = this._inst.exports.getsp(); // see comment above
|
||||||
storeValue(sp + 56, result);
|
storeValue(sp + 56, result);
|
||||||
mem().setUint8(sp + 64, 1);
|
this.mem.setUint8(sp + 64, 1);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
storeValue(sp + 56, 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);
|
const result = Reflect.apply(v, undefined, args);
|
||||||
sp = this._inst.exports.getsp(); // see comment above
|
sp = this._inst.exports.getsp(); // see comment above
|
||||||
storeValue(sp + 40, result);
|
storeValue(sp + 40, result);
|
||||||
mem().setUint8(sp + 48, 1);
|
this.mem.setUint8(sp + 48, 1);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
storeValue(sp + 40, 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);
|
const result = Reflect.construct(v, args);
|
||||||
sp = this._inst.exports.getsp(); // see comment above
|
sp = this._inst.exports.getsp(); // see comment above
|
||||||
storeValue(sp + 40, result);
|
storeValue(sp + 40, result);
|
||||||
mem().setUint8(sp + 48, 1);
|
this.mem.setUint8(sp + 48, 1);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
storeValue(sp + 40, 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
|
// func valueInstanceOf(v ref, t ref) bool
|
||||||
"syscall/js.valueInstanceOf": (sp) => {
|
"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)
|
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||||
|
@ -392,13 +448,13 @@
|
||||||
const dst = loadSlice(sp + 8);
|
const dst = loadSlice(sp + 8);
|
||||||
const src = loadValue(sp + 32);
|
const src = loadValue(sp + 32);
|
||||||
if (!(src instanceof Uint8Array)) {
|
if (!(src instanceof Uint8Array)) {
|
||||||
mem().setUint8(sp + 48, 0);
|
this.mem.setUint8(sp + 48, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const toCopy = src.subarray(0, dst.length);
|
const toCopy = src.subarray(0, dst.length);
|
||||||
dst.set(toCopy);
|
dst.set(toCopy);
|
||||||
setInt64(sp + 40, toCopy.length);
|
setInt64(sp + 40, toCopy.length);
|
||||||
mem().setUint8(sp + 48, 1);
|
this.mem.setUint8(sp + 48, 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
// func copyBytesToJS(dst ref, src []byte) (int, bool)
|
// func copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||||
|
@ -406,13 +462,13 @@
|
||||||
const dst = loadValue(sp + 8);
|
const dst = loadValue(sp + 8);
|
||||||
const src = loadSlice(sp + 16);
|
const src = loadSlice(sp + 16);
|
||||||
if (!(dst instanceof Uint8Array)) {
|
if (!(dst instanceof Uint8Array)) {
|
||||||
mem().setUint8(sp + 48, 0);
|
this.mem.setUint8(sp + 48, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const toCopy = src.subarray(0, dst.length);
|
const toCopy = src.subarray(0, dst.length);
|
||||||
dst.set(toCopy);
|
dst.set(toCopy);
|
||||||
setInt64(sp + 40, toCopy.length);
|
setInt64(sp + 40, toCopy.length);
|
||||||
mem().setUint8(sp + 48, 1);
|
this.mem.setUint8(sp + 48, 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
"debug": (value) => {
|
"debug": (value) => {
|
||||||
|
@ -424,7 +480,8 @@
|
||||||
|
|
||||||
async run(instance) {
|
async run(instance) {
|
||||||
this._inst = 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,
|
NaN,
|
||||||
0,
|
0,
|
||||||
null,
|
null,
|
||||||
|
@ -433,10 +490,10 @@
|
||||||
global,
|
global,
|
||||||
this,
|
this,
|
||||||
];
|
];
|
||||||
this._refs = new Map();
|
this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id
|
||||||
this.exited = false;
|
this._ids = new Map(); // mapping from JS values to reference ids
|
||||||
|
this._idPool = []; // unused ids that have been garbage collected
|
||||||
const mem = new DataView(this._inst.exports.mem.buffer)
|
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.
|
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
|
||||||
let offset = 4096;
|
let offset = 4096;
|
||||||
|
@ -444,7 +501,7 @@
|
||||||
const strPtr = (str) => {
|
const strPtr = (str) => {
|
||||||
const ptr = offset;
|
const ptr = offset;
|
||||||
const bytes = encoder.encode(str + "\0");
|
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;
|
offset += bytes.length;
|
||||||
if (offset % 8 !== 0) {
|
if (offset % 8 !== 0) {
|
||||||
offset += 8 - (offset % 8);
|
offset += 8 - (offset % 8);
|
||||||
|
@ -458,17 +515,18 @@
|
||||||
this.argv.forEach((arg) => {
|
this.argv.forEach((arg) => {
|
||||||
argvPtrs.push(strPtr(arg));
|
argvPtrs.push(strPtr(arg));
|
||||||
});
|
});
|
||||||
|
argvPtrs.push(0);
|
||||||
|
|
||||||
const keys = Object.keys(this.env).sort();
|
const keys = Object.keys(this.env).sort();
|
||||||
argvPtrs.push(keys.length);
|
|
||||||
keys.forEach((key) => {
|
keys.forEach((key) => {
|
||||||
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
|
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
|
||||||
});
|
});
|
||||||
|
argvPtrs.push(0);
|
||||||
|
|
||||||
const argv = offset;
|
const argv = offset;
|
||||||
argvPtrs.forEach((ptr) => {
|
argvPtrs.forEach((ptr) => {
|
||||||
mem.setUint32(offset, ptr, true);
|
this.mem.setUint32(offset, ptr, true);
|
||||||
mem.setUint32(offset + 4, 0, true);
|
this.mem.setUint32(offset + 4, 0, true);
|
||||||
offset += 8;
|
offset += 8;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ var (
|
||||||
Height = 768
|
Height = 768
|
||||||
|
|
||||||
// Cascade offset for creating multiple windows.
|
// Cascade offset for creating multiple windows.
|
||||||
Cascade = render.NewPoint(10, 10)
|
Cascade = render.NewPoint(10, 32)
|
||||||
CascadeStep = render.NewPoint(24, 24)
|
CascadeStep = render.NewPoint(24, 24)
|
||||||
CascadeLoops = 1
|
CascadeLoops = 1
|
||||||
|
|
||||||
|
@ -45,6 +45,20 @@ func main() {
|
||||||
panic(err)
|
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.
|
// Add some windows to play with.
|
||||||
addWindow(mw)
|
addWindow(mw)
|
||||||
addWindow(mw)
|
addWindow(mw)
|
||||||
|
@ -95,7 +109,7 @@ func addWindow(mw *ui.MainWindow) {
|
||||||
Cascade.Add(CascadeStep)
|
Cascade.Add(CascadeStep)
|
||||||
if Cascade.Y > Height-240-64 {
|
if Cascade.Y > Height-240-64 {
|
||||||
CascadeLoops++
|
CascadeLoops++
|
||||||
Cascade.Y = 24
|
Cascade.Y = 32
|
||||||
Cascade.X = 24 * CascadeLoops
|
Cascade.X = 24 * CascadeLoops
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ var (
|
||||||
Height = 768
|
Height = 768
|
||||||
|
|
||||||
// Cascade offset for creating multiple windows.
|
// Cascade offset for creating multiple windows.
|
||||||
Cascade = render.NewPoint(10, 10)
|
Cascade = render.NewPoint(10, 32)
|
||||||
CascadeStep = render.NewPoint(24, 24)
|
CascadeStep = render.NewPoint(24, 24)
|
||||||
CascadeLoops = 1
|
CascadeLoops = 1
|
||||||
|
|
||||||
|
@ -73,6 +73,20 @@ func main() {
|
||||||
height-lbl.Size().H-20,
|
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.
|
// Add some windows to play with.
|
||||||
addWindow(mw, frame, supervisor)
|
addWindow(mw, frame, supervisor)
|
||||||
addWindow(mw, frame, supervisor)
|
addWindow(mw, frame, supervisor)
|
||||||
|
@ -85,6 +99,7 @@ func main() {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frame.Present(mw, frame.Point())
|
||||||
lbl.Present(mw, lbl.Point())
|
lbl.Present(mw, lbl.Point())
|
||||||
supervisor.Loop(ev)
|
supervisor.Loop(ev)
|
||||||
supervisor.Present(mw)
|
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
|
// widgetInFocusedWindow returns whether a widget (like a Button) is a
|
||||||
// descendant of a Window that is being Window Managed by Supervisor, and
|
// descendant of a Window that is being Window Managed by Supervisor, and
|
||||||
// said window is in a Focused state.
|
// said window is in a Focused state.
|
||||||
|
|
206
menu.go
206
menu.go
|
@ -4,14 +4,20 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/go/render"
|
"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 {
|
type Menu struct {
|
||||||
BaseWidget
|
BaseWidget
|
||||||
Name string
|
Name string
|
||||||
|
|
||||||
|
supervisor *Supervisor
|
||||||
body *Frame
|
body *Frame
|
||||||
|
items []*MenuItem
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMenu creates a new Menu. It is hidden by default. Usually you'll
|
// NewMenu creates a new Menu. It is hidden by default. Usually you'll
|
||||||
|
@ -20,23 +26,56 @@ func NewMenu(name string) *Menu {
|
||||||
w := &Menu{
|
w := &Menu{
|
||||||
Name: name,
|
Name: name,
|
||||||
body: NewFrame(name + ":Body"),
|
body: NewFrame(name + ":Body"),
|
||||||
|
items: []*MenuItem{},
|
||||||
}
|
}
|
||||||
w.body.Configure(Config{
|
w.body.Configure(Config{
|
||||||
Width: 150,
|
Width: MenuWidth,
|
||||||
BorderSize: 12,
|
Height: 100,
|
||||||
|
BorderSize: 0,
|
||||||
BorderStyle: BorderRaised,
|
BorderStyle: BorderRaised,
|
||||||
Background: render.Grey,
|
Background: theme.ButtonBackgroundColor,
|
||||||
})
|
})
|
||||||
|
w.body.SetParent(w)
|
||||||
w.IDFunc(func() string {
|
w.IDFunc(func() string {
|
||||||
return fmt.Sprintf("Menu<%s>", w.Name)
|
return fmt.Sprintf("Menu<%s>", w.Name)
|
||||||
})
|
})
|
||||||
return w
|
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
|
// Compute the menu
|
||||||
func (w *Menu) Compute(e render.Engine) {
|
func (w *Menu) Compute(e render.Engine) {
|
||||||
w.body.Compute(e)
|
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.
|
// Call the BaseWidget Compute in case we have subscribers.
|
||||||
w.BaseWidget.Compute(e)
|
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.
|
// AddItem quickly adds an item to a menu.
|
||||||
func (w *Menu) AddItem(label string, command func()) *MenuItem {
|
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)
|
w.Pack(menu)
|
||||||
return 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.
|
// Pack a menu item onto the menu.
|
||||||
func (w *Menu) Pack(item *MenuItem) {
|
func (w *Menu) Pack(item *MenuItem) {
|
||||||
|
w.items = append(w.items, item)
|
||||||
w.body.Pack(item, Pack{
|
w.body.Pack(item, Pack{
|
||||||
Side: NE,
|
Side: N,
|
||||||
// Expand: true,
|
|
||||||
// Padding: 8,
|
|
||||||
FillX: true,
|
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.
|
// MenuItem is an item in a Menu.
|
||||||
|
@ -72,13 +161,19 @@ type MenuItem struct {
|
||||||
Label string
|
Label string
|
||||||
Accelerator string
|
Accelerator string
|
||||||
Command func()
|
Command func()
|
||||||
|
separator bool
|
||||||
button *Button
|
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.
|
// NewMenuItem creates a new menu item.
|
||||||
func NewMenuItem(label string, command func()) *MenuItem {
|
func NewMenuItem(label, accelerator string, command func()) *MenuItem {
|
||||||
w := &MenuItem{
|
w := &MenuItem{
|
||||||
Label: label,
|
Label: label,
|
||||||
|
Accelerator: accelerator,
|
||||||
Command: command,
|
Command: command,
|
||||||
}
|
}
|
||||||
w.IDFunc(func() string {
|
w.IDFunc(func() string {
|
||||||
|
@ -86,14 +181,51 @@ func NewMenuItem(label string, command func()) *MenuItem {
|
||||||
})
|
})
|
||||||
|
|
||||||
font := DefaultFont
|
font := DefaultFont
|
||||||
font.Color = render.White
|
font.Color = render.Black
|
||||||
font.PadX = 12
|
font.PadX = 12
|
||||||
w.Button.child = NewLabel(Label{
|
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,
|
Text: label,
|
||||||
Font: font,
|
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{
|
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 {
|
w.Button.Handle(Click, func(ed EventData) error {
|
||||||
|
@ -104,3 +236,53 @@ func NewMenuItem(label string, command func()) *MenuItem {
|
||||||
// Assign the button
|
// Assign the button
|
||||||
return w
|
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()
|
||||||
|
}
|
107
supervisor.go
107
supervisor.go
|
@ -32,6 +32,7 @@ const (
|
||||||
CloseWindow
|
CloseWindow
|
||||||
MaximizeWindow
|
MaximizeWindow
|
||||||
MinimizeWindow
|
MinimizeWindow
|
||||||
|
CloseModal
|
||||||
|
|
||||||
// Lifecycle event handlers.
|
// Lifecycle event handlers.
|
||||||
Compute // fired whenever the widget runs Compute
|
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 is the render engine on Compute and Present events.
|
||||||
Engine render.Engine
|
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
|
// 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
|
clicked map[int]bool // map of widgets being clicked
|
||||||
dd *DragDrop
|
dd *DragDrop
|
||||||
|
|
||||||
|
// Stack of modal widgets that have event priority.
|
||||||
|
modals []Widget
|
||||||
|
|
||||||
// List of window focus history for Window Manager.
|
// List of window focus history for Window Manager.
|
||||||
winFocus *FocusedWindow
|
winFocus *FocusedWindow
|
||||||
winTop *FocusedWindow // pointer to top-most window
|
winTop *FocusedWindow // pointer to top-most window
|
||||||
|
@ -76,6 +83,7 @@ func NewSupervisor() *Supervisor {
|
||||||
widgets: map[int]WidgetSlot{},
|
widgets: map[int]WidgetSlot{},
|
||||||
hovering: map[int]interface{}{},
|
hovering: map[int]interface{}{},
|
||||||
clicked: map[int]bool{},
|
clicked: map[int]bool{},
|
||||||
|
modals: []Widget{},
|
||||||
dd: NewDragDrop(),
|
dd: NewDragDrop(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,14 +177,18 @@ func (s *Supervisor) Loop(ev *event.State) error {
|
||||||
// Run events in managed windows first, from top to bottom.
|
// Run events in managed windows first, from top to bottom.
|
||||||
// Widgets in unmanaged windows will be handled next.
|
// Widgets in unmanaged windows will be handled next.
|
||||||
// err := s.runWindowEvents(XY, ev, hovering, outside)
|
// err := s.runWindowEvents(XY, ev, hovering, outside)
|
||||||
|
// 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)
|
handled, err := s.runWidgetEvents(XY, ev, hovering, outside, true)
|
||||||
if err == ErrStopPropagation || handled {
|
if err == ErrStopPropagation || handled {
|
||||||
// A widget in the active window has accepted an event. Do not pass
|
// A widget in the active window has accepted an event. Do not pass
|
||||||
// the event also to lower widgets.
|
// the event also to lower widgets.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Run events for the other widgets not in a managed window.
|
// 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)
|
s.runWidgetEvents(XY, ev, hovering, outside, false)
|
||||||
|
|
||||||
return nil
|
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
|
// 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.
|
// a part of a window, it gets no events triggered.
|
||||||
// 1: widgets are part of the active focused window.
|
// 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?
|
// Do we run any events?
|
||||||
var (
|
var (
|
||||||
stopPropagation bool
|
stopPropagation bool
|
||||||
ranEvents 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
|
// 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
|
// 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 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
|
// If the cursor is inside the box of the focused window, don't trigger
|
||||||
// active (hovering) mouse events. MouseOut type events, below, can still
|
// active (hovering) mouse events. MouseOut type events, below, can still
|
||||||
// trigger.
|
// trigger.
|
||||||
if cursorInsideFocusedWindow {
|
// Does not apply when a modal widget is active.
|
||||||
|
if cursorInsideFocusedWindow && modal == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,6 +308,14 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State, hovering,
|
||||||
continue
|
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.
|
// Check if the widget is part of a Window managed by Supervisor.
|
||||||
isManaged, isFocused := widgetInFocusedWindow(w)
|
isManaged, isFocused := widgetInFocusedWindow(w)
|
||||||
|
|
||||||
|
@ -344,6 +373,14 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State, hovering,
|
||||||
w = child.widget
|
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.
|
// Cursor is not intersecting the widget.
|
||||||
if _, ok := s.hovering[id]; ok {
|
if _, ok := s.hovering[id]; ok {
|
||||||
handle(w.Event(MouseOut, EventData{
|
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 a stopPropagation was called, return it up the stack.
|
||||||
if stopPropagation {
|
if stopPropagation {
|
||||||
return ranEvents, ErrStopPropagation
|
return ranEvents, ErrStopPropagation
|
||||||
|
@ -397,11 +444,28 @@ func (s *Supervisor) Present(e render.Engine) {
|
||||||
|
|
||||||
// Render the window manager windows from bottom to top.
|
// Render the window manager windows from bottom to top.
|
||||||
s.presentWindows(e)
|
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) {
|
func (s *Supervisor) Add(w Widget) {
|
||||||
s.lock.Lock()
|
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{
|
s.widgets[s.serial] = WidgetSlot{
|
||||||
id: s.serial,
|
id: s.serial,
|
||||||
widget: w,
|
widget: w,
|
||||||
|
@ -409,3 +473,40 @@ func (s *Supervisor) Add(w Widget) {
|
||||||
s.serial++
|
s.serial++
|
||||||
s.lock.Unlock()
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if parent, ok := w.Parent(); ok {
|
// Return if any parents are hidden.
|
||||||
return parent.Hidden()
|
parent, ok := w.Parent()
|
||||||
|
for ok {
|
||||||
|
if parent.Hidden() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
parent, ok = parent.Parent()
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
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.
|
// Children returns the window's child widgets.
|
||||||
func (w *Window) Children() []Widget {
|
func (w *Window) Children() []Widget {
|
||||||
return []Widget{
|
return []Widget{
|
||||||
|
|
|
@ -164,6 +164,21 @@ func (s *Supervisor) IsPointInWindow(point render.Point) bool {
|
||||||
return false
|
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.
|
// presentWindows draws the windows from bottom to top.
|
||||||
func (s *Supervisor) presentWindows(e render.Engine) {
|
func (s *Supervisor) presentWindows(e render.Engine) {
|
||||||
item := s.winBottom
|
item := s.winBottom
|
||||||
|
|
Loading…
Reference in New Issue
Block a user