Browse Source

Merge pull request 'Menus and Menu Bars' (#2) from menus into master

themes
Noah Petherbridge 1 year ago
parent
commit
53c0fed7be
20 changed files with 1140 additions and 121 deletions
  1. +7
    -11
      README.md
  2. +8
    -0
      debug.go
  3. BIN
      docs/menus-1.png
  4. BIN
      docs/menus-2.png
  5. +10
    -0
      eg/README.md
  6. +11
    -0
      eg/menus/Makefile
  7. +19
    -0
      eg/menus/README.md
  8. +208
    -0
      eg/menus/main.go
  9. +136
    -78
      eg/wasm-common/wasm_exec.js
  10. +16
    -2
      eg/windows/main.go
  11. +16
    -1
      eg/windows/main_wasm.go
  12. +13
    -0
      functions.go
  13. +201
    -19
      menu.go
  14. +80
    -0
      menu_bar.go
  15. +195
    -0
      menu_button.go
  16. +83
    -0
      menu_test.go
  17. +109
    -8
      supervisor.go
  18. +7
    -2
      widget.go
  19. +6
    -0
      window.go
  20. +15
    -0
      window_manager.go

+ 7
- 11
README.md View File

@@ -138,25 +138,21 @@ most complex.
(drag it by its title bar, Close button, window focus, multiple overlapping
windows, and so on).
* [x] **Tooltip**: a mouse hover label attached to a widget.
* [x] **MenuButton**: a button that opens a modal pop-up menu on click.
* [x] **MenuBar**: a specialized Frame that groups a bunch of MenuButtons and
provides a simple API to add menus and items to it.
* [x] **Menu**: a frame full of clickable links and separators. Usually used as
a modal pop-up by the MenuButton and MenuBar.

**Work in progress widgets:**

* [x] **Menu**: a frame with clickable menu items.
* To be a base widget behind right-click context menus, pull-down menus
from a MenuBar, options from a SelectBox and so on.
* Powered by Frame and Button but with a nice API for composing menu
actions.
* Partially implemented so far.
* [ ] **MenuButton**: a Button that opens a Menu when clicked.
* [ ] **MenuBar**: a Frame that houses many MenuButtons, intended for the
main menu at the top of a UI window (File, Edit, Help, etc.).
* [ ] **Scrollbar**: a Frame including a trough, scroll buttons and a
draggable slider.
* [ ] **SelectBox:** a kind of MenuButton that lets the user choose a value
from a list of possible values, bound to a string variable.

**Wish list for the longer-term future:**

* [ ] **SelectBox:** a kind of MenuButton that lets the user choose a value
from a list of possible values, bound to a string variable.
* [ ] **TextBox:** an editable text field that the user can focus and type
a value into.
* Would depend on the WindowManager to manage focus for the widgets.


+ 8
- 0
debug.go View File

@@ -5,6 +5,14 @@ import (
"strings"
)

// PrintWidgetTree prints a widget tree to console.
func PrintWidgetTree(root Widget) {
fmt.Printf("--- Widget Tree of %s ---\n", root)
for _, row := range WidgetTree(root) {
fmt.Println(row)
}
}

// WidgetTree returns a string representing the tree of widgets starting
// at a given widget.
func WidgetTree(root Widget) []string {


BIN
docs/menus-1.png View File

Before After
Width: 644  |  Height: 510  |  Size: 18 KiB

BIN
docs/menus-2.png View File

Before After
Width: 644  |  Height: 510  |  Size: 20 KiB

+ 10
- 0
eg/README.md 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
- 0
eg/menus/Makefile 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
- 0
eg/menus/README.md 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
- 0
eg/menus/main.go 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())
}

+ 136
- 78
eg/wasm-common/wasm_exec.js View File

@@ -30,6 +30,12 @@
global.fs = require("fs");
}

const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};

if (!global.fs) {
let outputBuf = "";
global.fs = {
@@ -45,27 +51,53 @@
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
throw new Error("not implemented");
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
open(path, flags, mode, callback) {
const err = new Error("not implemented");
err.code = "ENOSYS";
callback(err);
},
read(fd, buffer, offset, length, position, callback) {
const err = new Error("not implemented");
err.code = "ENOSYS";
callback(err);
},
fsync(fd, callback) {
callback(null);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}

if (!global.process) {
global.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}

if (!global.crypto) {
const nodeCrypto = require("crypto");
global.crypto = {
@@ -113,24 +145,19 @@
this._scheduledTimeouts = new Map();
this._nextCallbackTimeoutID = 1;

const mem = () => {
// The buffer may change when requesting more memory.
return new DataView(this._inst.exports.mem.buffer);
}

const setInt64 = (addr, v) => {
mem().setUint32(addr + 0, v, true);
mem().setUint32(addr + 4, Math.floor(v / 4294967296), true);
this.mem.setUint32(addr + 0, v, true);
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
}

const getInt64 = (addr) => {
const low = mem().getUint32(addr + 0, true);
const high = mem().getInt32(addr + 4, true);
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
}

const loadValue = (addr) => {
const f = mem().getFloat64(addr, true);
const f = this.mem.getFloat64(addr, true);
if (f === 0) {
return undefined;
}
@@ -138,7 +165,7 @@
return f;
}

const id = mem().getUint32(addr, true);
const id = this.mem.getUint32(addr, true);
return this._values[id];
}

@@ -147,57 +174,62 @@

if (typeof v === "number") {
if (isNaN(v)) {
mem().setUint32(addr + 4, nanHead, true);
mem().setUint32(addr, 0, true);
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
return;
}
if (v === 0) {
mem().setUint32(addr + 4, nanHead, true);
mem().setUint32(addr, 1, true);
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 1, true);
return;
}
mem().setFloat64(addr, v, true);
this.mem.setFloat64(addr, v, true);
return;
}

switch (v) {
case undefined:
mem().setFloat64(addr, 0, true);
this.mem.setFloat64(addr, 0, true);
return;
case null:
mem().setUint32(addr + 4, nanHead, true);
mem().setUint32(addr, 2, true);
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 2, true);
return;
case true:
mem().setUint32(addr + 4, nanHead, true);
mem().setUint32(addr, 3, true);
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 3, true);
return;
case false:
mem().setUint32(addr + 4, nanHead, true);
mem().setUint32(addr, 4, true);
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 4, true);
return;
}

let ref = this._refs.get(v);
if (ref === undefined) {
ref = this._values.length;
this._values.push(v);
this._refs.set(v, ref);
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
let typeFlag = 0;
this._goRefCounts[id]++;
let typeFlag = 1;
switch (typeof v) {
case "string":
typeFlag = 1;
typeFlag = 2;
break;
case "symbol":
typeFlag = 2;
typeFlag = 3;
break;
case "function":
typeFlag = 3;
typeFlag = 4;
break;
}
mem().setUint32(addr + 4, nanHead | typeFlag, true);
mem().setUint32(addr, ref, true);
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
}

const loadSlice = (addr) => {
@@ -232,11 +264,13 @@

// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
const code = mem().getInt32(sp + 8, true);
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
delete this._inst;
delete this._values;
delete this._refs;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
},

@@ -244,20 +278,25 @@
"runtime.wasmWrite": (sp) => {
const fd = getInt64(sp + 8);
const p = getInt64(sp + 16);
const n = mem().getInt32(sp + 24, true);
const n = this.mem.getInt32(sp + 24, true);
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
},

// func nanotime() int64
"runtime.nanotime": (sp) => {
// func resetMemoryDataView()
"runtime.resetMemoryDataView": (sp) => {
this.mem = new DataView(this._inst.exports.mem.buffer);
},

// func nanotime1() int64
"runtime.nanotime1": (sp) => {
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},

// func walltime() (sec int64, nsec int32)
"runtime.walltime": (sp) => {
// func walltime1() (sec int64, nsec int32)
"runtime.walltime1": (sp) => {
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
mem().setInt32(sp + 16, (msec % 1000) * 1000000, true);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},

// func scheduleTimeoutEvent(delay int64) int32
@@ -276,12 +315,12 @@
},
getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
));
mem().setInt32(sp + 16, id, true);
this.mem.setInt32(sp + 16, id, true);
},

// func clearTimeoutEvent(id int32)
"runtime.clearTimeoutEvent": (sp) => {
const id = mem().getInt32(sp + 8, true);
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
this._scheduledTimeouts.delete(id);
},
@@ -291,6 +330,18 @@
crypto.getRandomValues(loadSlice(sp + 8));
},

// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
},

// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
storeValue(sp + 24, loadString(sp + 8));
@@ -308,6 +359,11 @@
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
},

// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (sp) => {
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
},

// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (sp) => {
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
@@ -327,10 +383,10 @@
const result = Reflect.apply(m, v, args);
sp = this._inst.exports.getsp(); // see comment above
storeValue(sp + 56, result);
mem().setUint8(sp + 64, 1);
this.mem.setUint8(sp + 64, 1);
} catch (err) {
storeValue(sp + 56, err);
mem().setUint8(sp + 64, 0);
this.mem.setUint8(sp + 64, 0);
}
},

@@ -342,10 +398,10 @@
const result = Reflect.apply(v, undefined, args);
sp = this._inst.exports.getsp(); // see comment above
storeValue(sp + 40, result);
mem().setUint8(sp + 48, 1);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
storeValue(sp + 40, err);
mem().setUint8(sp + 48, 0);
this.mem.setUint8(sp + 48, 0);
}
},

@@ -357,10 +413,10 @@
const result = Reflect.construct(v, args);
sp = this._inst.exports.getsp(); // see comment above
storeValue(sp + 40, result);
mem().setUint8(sp + 48, 1);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
storeValue(sp + 40, err);
mem().setUint8(sp + 48, 0);
this.mem.setUint8(sp + 48, 0);
}
},

@@ -384,7 +440,7 @@

// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => {
mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16));
this.mem.setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16));
},

// func copyBytesToGo(dst []byte, src ref) (int, bool)
@@ -392,13 +448,13 @@
const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
if (!(src instanceof Uint8Array)) {
mem().setUint8(sp + 48, 0);
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
mem().setUint8(sp + 48, 1);
this.mem.setUint8(sp + 48, 1);
},

// func copyBytesToJS(dst ref, src []byte) (int, bool)
@@ -406,13 +462,13 @@
const dst = loadValue(sp + 8);
const src = loadSlice(sp + 16);
if (!(dst instanceof Uint8Array)) {
mem().setUint8(sp + 48, 0);
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
mem().setUint8(sp + 48, 1);
this.mem.setUint8(sp + 48, 1);
},

"debug": (value) => {
@@ -424,7 +480,8 @@

async run(instance) {
this._inst = instance;
this._values = [ // TODO: garbage collection
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
@@ -433,10 +490,10 @@
global,
this,
];
this._refs = new Map();
this.exited = false;
const mem = new DataView(this._inst.exports.mem.buffer)
this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map(); // mapping from JS values to reference ids
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited

// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
@@ -444,7 +501,7 @@
const strPtr = (str) => {
const ptr = offset;
const bytes = encoder.encode(str + "\0");
new Uint8Array(mem.buffer, offset, bytes.length).set(bytes);
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
offset += bytes.length;
if (offset % 8 !== 0) {
offset += 8 - (offset % 8);
@@ -458,17 +515,18 @@
this.argv.forEach((arg) => {
argvPtrs.push(strPtr(arg));
});
argvPtrs.push(0);

const keys = Object.keys(this.env).sort();
argvPtrs.push(keys.length);
keys.forEach((key) => {
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
});
argvPtrs.push(0);

const argv = offset;
argvPtrs.forEach((ptr) => {
mem.setUint32(offset, ptr, true);
mem.setUint32(offset + 4, 0, true);
this.mem.setUint32(offset, ptr, true);
this.mem.setUint32(offset + 4, 0, true);
offset += 8;
});



+ 16
- 2
eg/windows/main.go View File

@@ -17,7 +17,7 @@ var (
Height = 768

// Cascade offset for creating multiple windows.
Cascade = render.NewPoint(10, 10)
Cascade = render.NewPoint(10, 32)
CascadeStep = render.NewPoint(24, 24)
CascadeLoops = 1

@@ -45,6 +45,20 @@ func main() {
panic(err)
}

// Menu bar.
menu := ui.NewMenuBar("Main Menu")
file := menu.AddMenu("Options")
file.AddItem("New window", func() {
addWindow(mw)
})
file.AddItem("Close all windows", func() {
OpenWindows -= mw.Supervisor().CloseAllWindows()
})

menu.Supervise(mw.Supervisor())
menu.Compute(mw.Engine)
mw.Pack(menu, menu.PackTop())

// Add some windows to play with.
addWindow(mw)
addWindow(mw)
@@ -95,7 +109,7 @@ func addWindow(mw *ui.MainWindow) {
Cascade.Add(CascadeStep)
if Cascade.Y > Height-240-64 {
CascadeLoops++
Cascade.Y = 24
Cascade.Y = 32
Cascade.X = 24 * CascadeLoops
}



+ 16
- 1
eg/windows/main_wasm.go View File

@@ -24,7 +24,7 @@ var (
Height = 768

// Cascade offset for creating multiple windows.
Cascade = render.NewPoint(10, 10)
Cascade = render.NewPoint(10, 32)
CascadeStep = render.NewPoint(24, 24)
CascadeLoops = 1

@@ -73,6 +73,20 @@ func main() {
height-lbl.Size().H-20,
))

// Menu bar.
menu := ui.NewMenuBar("Main Menu")
file := menu.AddMenu("Options")
file.AddItem("New window", func() {
addWindow(mw, frame, supervisor)
})
file.AddItem("Close all windows", func() {
OpenWindows -= supervisor.CloseAllWindows()
})

menu.Supervise(supervisor)
menu.Compute(mw)
frame.Pack(menu, menu.PackTop())

// Add some windows to play with.
addWindow(mw, frame, supervisor)
addWindow(mw, frame, supervisor)
@@ -85,6 +99,7 @@ func main() {
panic(err)
}

frame.Present(mw, frame.Point())
lbl.Present(mw, lbl.Point())
supervisor.Loop(ev)
supervisor.Present(mw)


+ 13
- 0
functions.go 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
// descendant of a Window that is being Window Managed by Supervisor, and
// said window is in a Focused state.


+ 201
- 19
menu.go View File

@@ -4,39 +4,78 @@ import (
"fmt"

"git.kirsle.net/go/render"
"git.kirsle.net/go/ui/theme"
)

// Menu is a rectangle that holds menu items.
// MenuWidth sets the width of all popup menus. TODO, widths should be automatic.
var MenuWidth = 180

// Menu is a frame that holds menu items. It is the
type Menu struct {
BaseWidget
Name string

body *Frame
supervisor *Supervisor
body *Frame
items []*MenuItem
}

// NewMenu creates a new Menu. It is hidden by default. Usually you'll
// use it with a MenuButton or in a right-click handler.
func NewMenu(name string) *Menu {
w := &Menu{
Name: name,
body: NewFrame(name + ":Body"),
Name: name,
body: NewFrame(name + ":Body"),
items: []*MenuItem{},
}
w.body.Configure(Config{
Width: 150,
BorderSize: 12,
Width: MenuWidth,
Height: 100,
BorderSize: 0,
BorderStyle: BorderRaised,
Background: render.Grey,
Background: theme.ButtonBackgroundColor,
})
w.body.SetParent(w)
w.IDFunc(func() string {
return fmt.Sprintf("Menu<%s>", w.Name)
})
return w
}

// Children returns the child frame of the menu.
func (w *Menu) Children() []Widget {
return []Widget{
w.body,
}
}

// Supervise the Menu. This will add all current and future MenuItem widgets
// to the supervisor.
func (w *Menu) Supervise(s *Supervisor) {
w.supervisor = s
for _, item := range w.items {
w.supervisor.Add(item)
}
}

// Compute the menu
func (w *Menu) Compute(e render.Engine) {
w.body.Compute(e)

// TODO: ideally the Frame Pack Compute would fix the size of the body
// for the height to match the height of the menu items... but for now
// manually set the height.
var maxWidth int
var height int
for _, child := range w.body.Children() {
size := child.Size()
if size.W > maxWidth {
maxWidth = size.W
}
height += child.Size().H
}
w.body.Resize(render.NewRect(maxWidth, height))

// Call the BaseWidget Compute in case we have subscribers.
w.BaseWidget.Compute(e)
}
@@ -51,19 +90,69 @@ func (w *Menu) Present(e render.Engine, p render.Point) {

// AddItem quickly adds an item to a menu.
func (w *Menu) AddItem(label string, command func()) *MenuItem {
menu := NewMenuItem(label, command)
menu := NewMenuItem(label, "", command)

// Add a Click handler that closes the menu when a selection is made.
menu.Handle(Click, w.menuClickHandler)

w.Pack(menu)
return menu
}

// AddItemAccel quickly adds an item to a menu with a shortcut key label.
func (w *Menu) AddItemAccel(label string, accelerator string, command func()) *MenuItem {
menu := NewMenuItem(label, accelerator, command)

// Add a Click handler that closes the menu when a selection is made.
menu.Handle(Click, w.menuClickHandler)

w.Pack(menu)
return menu
}

// Click handler for all menu items, to also close the menu behind them.
func (w *Menu) menuClickHandler(ed EventData) error {
if w.supervisor != nil {
w.supervisor.PopModal(w)
}
return nil
}

// AddSeparator adds a separator bar to the menu to delineate items.
func (w *Menu) AddSeparator() *MenuItem {
sep := NewMenuSeparator()
w.Pack(sep)
return sep
}

// Pack a menu item onto the menu.
func (w *Menu) Pack(item *MenuItem) {
w.items = append(w.items, item)
w.body.Pack(item, Pack{
Side: NE,
// Expand: true,
// Padding: 8,
Side: N,
FillX: true,
})
if w.supervisor != nil {
w.supervisor.Add(item)
}
}

// Size returns the size of the menu's body.
func (w *Menu) Size() render.Rect {
return w.body.Size()
}

// Rect returns the rect of the menu's body.
func (w *Menu) Rect() render.Rect {
// TODO: the height reports wrong (0), manually add up the MenuItem sizes.
// This manifests in Supervisor.runWidgetEvents when checking if the cursor
// clicked outside the rect of the active menu modal.
rect := w.body.Rect()
rect.H = 0
for _, child := range w.body.Children() {
rect.H += child.Size().H
}
return rect
}

// MenuItem is an item in a Menu.
@@ -72,28 +161,71 @@ type MenuItem struct {
Label string
Accelerator string
Command func()
separator bool
button *Button

// store of most recent bg color set on a menu item
cacheBg render.Color
cacheFg render.Color
}

// NewMenuItem creates a new menu item.
func NewMenuItem(label string, command func()) *MenuItem {
func NewMenuItem(label, accelerator string, command func()) *MenuItem {
w := &MenuItem{
Label: label,
Command: command,
Label: label,
Accelerator: accelerator,
Command: command,
}
w.IDFunc(func() string {
return fmt.Sprintf("MenuItem<%s>", w.Label)
})

font := DefaultFont
font.Color = render.White
font.Color = render.Black
font.PadX = 12
w.Button.child = NewLabel(Label{
Text: label,
Font: font,
font.PadY = 2

// The button child will be a Frame so we can have a left-aligned label
// and a right-aligned accelerator.
frame := NewFrame(label + ":Frame")
frame.Configure(Config{
Width: MenuWidth,
})
{
// Left of frame: menu item label
lbl := NewLabel(Label{
Text: label,
Font: font,
})
frame.Pack(lbl, Pack{
Side: W,
})

// On the right: accelerator shortcut key
if accelerator != "" {
accel := NewLabel(Label{
Text: accelerator,
Font: font,
})
frame.Pack(accel, Pack{
Side: E,
})
}
}

w.Button.child = frame
w.Button.Configure(Config{
Background: render.Blue,
BorderSize: 0,
Background: theme.ButtonBackgroundColor,
})

w.Button.Handle(MouseOver, func(ed EventData) error {
w.setHoverStyle(true)
return nil
})
w.Button.Handle(MouseOut, func(ed EventData) error {
w.setHoverStyle(false)
return nil
})

w.Button.Handle(Click, func(ed EventData) error {
@@ -104,3 +236,53 @@ func NewMenuItem(label string, command func()) *MenuItem {
// Assign the button
return w
}

// NewMenuSeparator creates a separator menu item.
func NewMenuSeparator() *MenuItem {
w := &MenuItem{
separator: true,
}
w.IDFunc(func() string {
return "MenuItem<separator>"
})
w.Button.child = NewFrame("Menu Separator")
w.Button.Configure(Config{
Width: MenuWidth,
Height: 2,
BorderSize: 1,
BorderStyle: BorderSunken,
BorderColor: render.Grey,
})
return w
}

// Set the hover styling (text/bg color)
func (w *MenuItem) setHoverStyle(hovering bool) {
// Note: this only works if the MenuItem is using the standard
// Frame and Labels layout created by AddItem(). If not, this function
// does nothing.

// BG color.
if hovering {
w.cacheBg = w.Background()
w.SetBackground(render.SkyBlue)
} else {
w.SetBackground(w.cacheBg)
}

frame, ok := w.Button.child.(*Frame)
if !ok {
return
}

for _, widget := range frame.Children() {
if label, ok := widget.(*Label); ok {
if hovering {
w.cacheFg = label.Font.Color
label.Font.Color = render.White
} else {
label.Font.Color = w.cacheFg
}
}
}
}

+ 80
- 0
menu_bar.go 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
- 0
menu_button.go 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
- 0
menu_test.go 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()
}

+ 109
- 8
supervisor.go View File

@@ -32,6 +32,7 @@ const (
CloseWindow
MaximizeWindow
MinimizeWindow
CloseModal

// Lifecycle event handlers.
Compute // fired whenever the widget runs Compute
@@ -45,6 +46,9 @@ type EventData struct {

// Engine is the render engine on Compute and Present events.
Engine render.Engine

// Supervisor is the reference to the supervisor who sent the event.
Supervisor *Supervisor
}

// Supervisor keeps track of widgets of interest to notify them about
@@ -58,6 +62,9 @@ type Supervisor struct {
clicked map[int]bool // map of widgets being clicked
dd *DragDrop

// Stack of modal widgets that have event priority.
modals []Widget

// List of window focus history for Window Manager.
winFocus *FocusedWindow
winTop *FocusedWindow // pointer to top-most window
@@ -76,6 +83,7 @@ func NewSupervisor() *Supervisor {
widgets: map[int]WidgetSlot{},
hovering: map[int]interface{}{},
clicked: map[int]bool{},
modals: []Widget{},
dd: NewDragDrop(),
}
}
@@ -169,14 +177,18 @@ func (s *Supervisor) Loop(ev *event.State) error {
// Run events in managed windows first, from top to bottom.
// Widgets in unmanaged windows will be handled next.
// err := s.runWindowEvents(XY, ev, hovering, outside)
handled, err := s.runWidgetEvents(XY, ev, hovering, outside, true)
if err == ErrStopPropagation || handled {
// A widget in the active window has accepted an event. Do not pass
// the event also to lower widgets.
return err
// Only run if there is no active modal (modals have top priority)
if len(s.modals) == 0 {
handled, err := s.runWidgetEvents(XY, ev, hovering, outside, true)
if err == ErrStopPropagation || handled {
// A widget in the active window has accepted an event. Do not pass
// the event also to lower widgets.
return err
}
}

// Run events for the other widgets not in a managed window.
// (Modal event priority is handled in runWidgetEvents)
s.runWidgetEvents(XY, ev, hovering, outside, false)

return nil
@@ -233,13 +245,21 @@ func (s *Supervisor) Hovering(cursor render.Point) (hovering, outside []WidgetSl
// 0: widgets NOT part of a managed window. On this pass, if a widget IS
// a part of a window, it gets no events triggered.
// 1: widgets are part of the active focused window.
func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State, hovering, outside []WidgetSlot, toFocusedWindow bool) (bool, error) {
func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State,
hovering, outside []WidgetSlot, toFocusedWindow bool) (bool, error) {
// Do we run any events?
var (
stopPropagation bool
ranEvents bool
)

// Do we have active modals? Modal widgets have top event priority given
// only to the top-most modal.
var modal Widget
if len(s.modals) > 0 {
modal = s.modals[len(s.modals)-1]
}

// If we're running this method in "Phase 2" (to widgets NOT in the focused
// window), only send mouse events to widgets if the cursor is NOT inside
// the bounding box of the active focused window. Prevents clicking "thru"
@@ -273,7 +293,8 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State, hovering,
// If the cursor is inside the box of the focused window, don't trigger
// active (hovering) mouse events. MouseOut type events, below, can still
// trigger.
if cursorInsideFocusedWindow {
// Does not apply when a modal widget is active.
if cursorInsideFocusedWindow && modal == nil {
break
}

@@ -287,6 +308,14 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State, hovering,
continue
}

// If we have a modal active, validate this widget is a child of
// the modal widget.
if modal != nil {
if !HasParent(w, modal) {
continue
}
}

// Check if the widget is part of a Window managed by Supervisor.
isManaged, isFocused := widgetInFocusedWindow(w)

@@ -344,6 +373,14 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State, hovering,
w = child.widget
)

// If we have a modal active, validate this widget is a child of
// the modal widget.
if modal != nil {
if !HasParent(w, modal) {
continue
}
}

// Cursor is not intersecting the widget.
if _, ok := s.hovering[id]; ok {
handle(w.Event(MouseOut, EventData{
@@ -360,6 +397,16 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State, hovering,
}
}

// If a modal is active and a click was registered outside the modal's
// bounding box, send the CloseModal event.
if modal != nil && !XY.Inside(AbsoluteRect(modal)) {
if ev.Button1 {
modal.Event(CloseModal, EventData{
Supervisor: s,
})
}
}

// If a stopPropagation was called, return it up the stack.
if stopPropagation {
return ranEvents, ErrStopPropagation
@@ -397,11 +444,28 @@ func (s *Supervisor) Present(e render.Engine) {

// Render the window manager windows from bottom to top.
s.presentWindows(e)

// Render the modals from bottom to top.
if len(s.modals) > 0 {
for _, modal := range s.modals {
modal.Present(e, modal.Point())
}
}
}

// Add a widget to be supervised.
// Add a widget to be supervised. Has no effect if the widget is already
// under the supervisor's care.
func (s *Supervisor) Add(w Widget) {
s.lock.Lock()

// Check it's not already there.
for _, child := range s.widgets {
if child.widget == w {
return
}
}

// Add it.
s.widgets[s.serial] = WidgetSlot{
id: s.serial,
widget: w,
@@ -409,3 +473,40 @@ func (s *Supervisor) Add(w Widget) {
s.serial++
s.lock.Unlock()
}

// PushModal sets the widget to be a "modal" for the Supervisor.
//
// Modal widgets have top-most event priority: mouse and click events go ONLY
// to the modal and its descendants. Modals work as a stack: the most recently
// pushed widget is the active modal, and popping the modal will make the
// next most-recent widget be the active modal.
//
// If a Click event registers OUTSIDE the bounds of the modal widget, the
// widget receives a CloseModal event.
//
// Returns the length of the modal stack.
func (s *Supervisor) PushModal(w Widget) int {
s.modals = append(s.modals, w)
return len(s.modals)
}

// PopModal attempts to pop the modal from the stack, but only if the modal
// is at the top of the stack.
//
// A widget may safely attempt to PopModal itself on a CloseModal event to
// close themselves when the user clicks outside their box. If there were a
// newer modal on the stack, this PopModal action would do nothing.
func (s *Supervisor) PopModal(w Widget) bool {
// only can pop if the topmost widget is the one being asked for
if len(s.modals) > 0 && s.modals[len(s.modals)-1] == w {
modal := s.modals[len(s.modals)-1]
modal.Hide()

// pop it off
s.modals = s.modals[:len(s.modals)-1]

return true
}

return false
}

+ 7
- 2
widget.go View File

@@ -309,8 +309,13 @@ func (w *BaseWidget) Hidden() bool {
return true
}

if parent, ok := w.Parent(); ok {
return parent.Hidden()
// Return if any parents are hidden.
parent, ok := w.Parent()
for ok {
if parent.Hidden() {
return true
}
parent, ok = parent.Parent()
}

return false


+ 6
- 0
window.go 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.
func (w *Window) Children() []Widget {
return []Widget{


+ 15
- 0
window_manager.go View File

@@ -164,6 +164,21 @@ func (s *Supervisor) IsPointInWindow(point render.Point) bool {
return false
}

// CloseAllWindows closes all open windows being managed by supervisor.
// Returns the number of windows closed.
func (s *Supervisor) CloseAllWindows() int {
var (
node = s.winFocus
i = 0
)
for node != nil {
i++
node.window.Hide()
node = node.next
}
return i
}

// presentWindows draws the windows from bottom to top.
func (s *Supervisor) presentWindows(e render.Engine) {
item := s.winBottom


Loading…
Cancel
Save