Menus and Menu Bars

* New and completed widgets: Menu, MenuButton and MenuBar.
* MenuButton is a kind of Button that opens a popup Menu when clicked.
* MenuBar is a container of buttons designed to be attached to the top
  of an application window ("File, Edit, View, Help")
* Supervisor manages the popup menus with its new concept of a Modal
  Widget. Modal widgets take exclusive event priority for all mouse and
  key events. The pop-up menu is a modal window, which means you must
  click an option inside the menu OR clicking outside the menu will
  close it and eat your click event (widgets outside the modal don't
  receive events, but the modal itself gets an event that you've done
  this).
This commit is contained in:
Noah 2020-06-04 00:50:06 -07:00
parent d27636ea48
commit 07cefb6499
20 changed files with 1141 additions and 122 deletions

View File

@ -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.

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
docs/menus-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

10
eg/README.md Normal file
View 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
View 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
View 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
View 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())
}

View File

@ -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;
}
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) { 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;
}); });

View File

@ -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
} }

View File

@ -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)

View File

@ -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.

220
menu.go
View File

@ -4,39 +4,78 @@ 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
body *Frame supervisor *Supervisor
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
// use it with a MenuButton or in a right-click handler. // use it with a MenuButton or in a right-click handler.
func NewMenu(name string) *Menu { 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,28 +161,71 @@ 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,
Command: command, Accelerator: accelerator,
Command: command,
} }
w.IDFunc(func() string { w.IDFunc(func() string {
return fmt.Sprintf("MenuItem<%s>", w.Label) return fmt.Sprintf("MenuItem<%s>", w.Label)
}) })
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
Text: label,
Font: font, // 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{ 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
View 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
View 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
View 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()
}

View File

@ -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)
handled, err := s.runWidgetEvents(XY, ev, hovering, outside, true) // Only run if there is no active modal (modals have top priority)
if err == ErrStopPropagation || handled { if len(s.modals) == 0 {
// A widget in the active window has accepted an event. Do not pass handled, err := s.runWidgetEvents(XY, ev, hovering, outside, true)
// the event also to lower widgets. if err == ErrStopPropagation || handled {
return err // 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. // 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
}

View File

@ -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

View File

@ -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{

View File

@ -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