1167 lines
36 KiB
Go
1167 lines
36 KiB
Go
// +build gui
|
|
|
|
package main
|
|
|
|
/*
|
|
TODO:
|
|
Add clipboard stuff: https://developer.fyne.io/api/v2.1/clipboard.html
|
|
|
|
*/
|
|
|
|
import (
|
|
"fmt"
|
|
"image/color"
|
|
"net/url"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/app"
|
|
"fyne.io/fyne/v2/canvas"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/dialog"
|
|
"fyne.io/fyne/v2/driver/desktop"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/storage"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
)
|
|
|
|
type GUI struct {
|
|
gui fyne.App
|
|
windows map[string]fyne.Window
|
|
}
|
|
|
|
func (g GUI) String() string {
|
|
return "GUI-Root"
|
|
}
|
|
|
|
type guiWidget fyne.Widget
|
|
|
|
type guiContainer fyne.CanvasObject
|
|
|
|
type guiObject interface{}
|
|
|
|
type slopeTheme struct{}
|
|
|
|
func (t slopeTheme) String() string {
|
|
return "GUI-Theme"
|
|
}
|
|
|
|
// Dev note: no memory of why this is
|
|
// done this way.
|
|
var _ fyne.Theme = (*slopeTheme)(nil)
|
|
|
|
func (m slopeTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
|
|
if name == theme.ColorNameBackground {
|
|
if variant == theme.VariantLight {
|
|
return color.RGBA{200, 200, 200, 255}
|
|
}
|
|
return color.RGBA{60, 60, 60, 255}
|
|
}
|
|
|
|
return theme.DefaultTheme().Color(name, variant)
|
|
}
|
|
|
|
func (m slopeTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
|
|
return theme.DefaultTheme().Icon(name)
|
|
}
|
|
|
|
func (m slopeTheme) Font(style fyne.TextStyle) fyne.Resource {
|
|
return theme.DefaultTheme().Font(style)
|
|
}
|
|
|
|
func (m slopeTheme) Size(name fyne.ThemeSizeName) float32 {
|
|
return theme.DefaultTheme().Size(name) * 0.9
|
|
}
|
|
|
|
var guiLib = vars{
|
|
"gui-create": func(a ...expression) expression {
|
|
app := &GUI{app.New(), make(map[string]fyne.Window)}
|
|
app.gui.Settings().SetTheme(&slopeTheme{})
|
|
return app
|
|
},
|
|
"gui-add-window": func(a ...expression) expression {
|
|
if len(a) < 2 {
|
|
return exception("'gui-add-window' expects a gui-root and a window name string as arguments, too few arguments were given")
|
|
}
|
|
g, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'gui-add-window' was given a non-gui-root value as its first argument")
|
|
}
|
|
s := String(a[1], false)
|
|
g.windows[s] = g.gui.NewWindow(s)
|
|
return true
|
|
},
|
|
"gui-set-char-handlers": func(a ...expression) expression {
|
|
if len(a) < 3 {
|
|
return exception("'gui-set-char-handlers' expects a gui-root, a window-name string, a callback lambda, a key string, and an optional modifier string; too few arguments were given")
|
|
}
|
|
g, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'gui-set-char-handlers' was given a non-gui-root value as its first argument")
|
|
}
|
|
s := String(a[1], false)
|
|
window, ok := g.windows[s]
|
|
if !ok {
|
|
return exception("'gui-set-char-handlers' was given a window name that does not exist in the given gui-root")
|
|
}
|
|
|
|
// Callback
|
|
switch f := a[2].(type) {
|
|
case func(...expression) expression:
|
|
window.Canvas().SetOnTypedRune(func(r rune) { f(string(r)) })
|
|
return true
|
|
case proc:
|
|
window.Canvas().SetOnTypedRune(func(r rune) { apply(f, []expression{string(r)}, "Custom gui-char-handler") })
|
|
return true
|
|
default:
|
|
return exception("'gui-set-char-handlers' was given a non-procedure (lambda or builtin) value for its third argument")
|
|
}
|
|
|
|
},
|
|
"gui-add-shortcut": func(a ...expression) expression {
|
|
// (gui-add-shortcut [gui-root] [window-id] [callback] [key: string] [modifier: string])
|
|
if len(a) < 5 {
|
|
return exception("'gui-add-shortcut' expects a gui-root, a window-name string, a callback lambda, a key string, and a modifier string; too few arguments were given")
|
|
}
|
|
g, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'gui-add-shortcut' was given a non-gui-root value as its first argument")
|
|
}
|
|
s := String(a[1], false)
|
|
window, ok := g.windows[s]
|
|
if !ok {
|
|
return exception("'gui-add-shortcut' was given a window name that does not exist in the given gui-root")
|
|
}
|
|
|
|
key := String(a[3], false)
|
|
modStr := strings.ToLower(String(a[4], false))
|
|
mod := desktop.ShiftModifier
|
|
|
|
switch modStr {
|
|
case "shift", "sh":
|
|
mod = desktop.ShiftModifier
|
|
case "control", "ctrl", "ctl", "strg":
|
|
mod = desktop.ControlModifier
|
|
case "alt":
|
|
mod = desktop.AltModifier
|
|
case "super", "sup", "windows", "win", "com", "command":
|
|
mod = desktop.SuperModifier
|
|
default:
|
|
return exception("'gui-add-shortcut' was given an unknown modifier")
|
|
}
|
|
|
|
// Callback
|
|
switch f := a[2].(type) {
|
|
case func(...expression) expression:
|
|
window.Canvas().AddShortcut(&desktop.CustomShortcut{fyne.KeyName(key), mod}, func(shortcut fyne.Shortcut) { f() })
|
|
return true
|
|
case proc:
|
|
window.Canvas().AddShortcut(&desktop.CustomShortcut{fyne.KeyName(key), desktop.Modifier(2)}, func(shortcut fyne.Shortcut) { apply(f, []expression{}, "Custom gui-shortcut-handler") })
|
|
return true
|
|
default:
|
|
return exception("'gui-add-shortcut' was given a non-procedure (lambda or builtin) value for its third argument")
|
|
}
|
|
|
|
},
|
|
"gui-list-windows": func(a ...expression) expression {
|
|
if len(a) == 0 {
|
|
return exception("'gui-list-windows' expects a gui-root as an argument, no arguments were given")
|
|
}
|
|
g, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'gui-list-windows' was given a non-gui-root argument")
|
|
}
|
|
out := make([]expression, 0, len(g.windows))
|
|
for k := range g.windows {
|
|
out = append(out, k)
|
|
}
|
|
return out
|
|
},
|
|
"gui-use-light-theme": func(a ...expression) expression {
|
|
if len(a) == 0 {
|
|
return exception("'gui-use-light-theme' expects a gui-root as its first argument and, optionally, a bool as its second argument, no arguments were given")
|
|
}
|
|
|
|
app, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'gui-use-light-theme' expects a gui-root as its first argument, a non-gui-root value was given")
|
|
}
|
|
|
|
// If the current theme is == to the light theme return true, else false
|
|
if len(a) == 1 {
|
|
return reflect.DeepEqual(app.gui.Settings().Theme(), theme.LightTheme())
|
|
}
|
|
|
|
b := AnythingToBool(a[1]).(bool)
|
|
if b {
|
|
app.gui.Settings().SetTheme(theme.LightTheme())
|
|
} else {
|
|
app.gui.Settings().SetTheme(theme.DarkTheme())
|
|
}
|
|
return b
|
|
},
|
|
"window-set-content": func(a ...expression) expression {
|
|
// (window-set-content window-id content...)
|
|
if len(a) < 3 {
|
|
return exception("'window-set-content' expects a gui-root and a string as arguments, too few arguments were given")
|
|
}
|
|
g, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'window-set-content' was given a non-gui-root value as its first argument")
|
|
}
|
|
s := String(a[1], false)
|
|
w, ok := g.windows[s]
|
|
if !ok {
|
|
return exception("'window-set-content' was given a window-id that does not exist: " + s)
|
|
}
|
|
content, ok := a[2].(fyne.CanvasObject)
|
|
if !ok {
|
|
return exception("'window-set-content' was given invalid content")
|
|
}
|
|
w.SetContent(content)
|
|
return true
|
|
},
|
|
"window-resize": func(a ...expression) expression {
|
|
if len(a) < 4 {
|
|
return exception("'window-resize' expects a GUI, a string, and two numbers as arguments; too few arguments were given")
|
|
}
|
|
g, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'window-resize' was given a non-GUI value as its first argument")
|
|
}
|
|
s := String(a[1], false)
|
|
w, ok := g.windows[s]
|
|
if !ok {
|
|
return exception("'window-resize' was given a window-id that does not exist: " + s)
|
|
}
|
|
width, ok := a[2].(number)
|
|
height, ok2 := a[3].(number)
|
|
if !ok || !ok2 {
|
|
return exception("'window-resize' was given a non-number value for height or width")
|
|
}
|
|
w.Resize(fyne.NewSize(float32(width), float32(height)))
|
|
return true
|
|
},
|
|
"window-show": func(a ...expression) expression {
|
|
if len(a) < 2 {
|
|
return exception("'window-show' expects a GUI and a string; too few arguments were given")
|
|
}
|
|
g, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'window-show' was given a non-GUI value as its first argument")
|
|
}
|
|
s := String(a[1], false)
|
|
w, ok := g.windows[s]
|
|
if !ok {
|
|
return exception("'window-show' was given a window-id that does not exist: " + s)
|
|
}
|
|
w.Show()
|
|
return true
|
|
},
|
|
"window-hide": func(a ...expression) expression {
|
|
if len(a) < 2 {
|
|
return exception("'window-hide' expects a GUI and a string; too few arguments were given")
|
|
}
|
|
g, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'window-hide' was given a non-GUI value as its first argument")
|
|
}
|
|
s := String(a[1], false)
|
|
w, ok := g.windows[s]
|
|
if !ok {
|
|
return exception("'window-hide' was given a window-id that does not exist: " + s)
|
|
}
|
|
w.Hide()
|
|
return true
|
|
},
|
|
"window-close": func(a ...expression) expression {
|
|
if len(a) < 2 {
|
|
return exception("'window-close' expects a GUI and a string; too few arguments were given")
|
|
}
|
|
g, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'window-close' was given a non-GUI value as its first argument")
|
|
}
|
|
s := String(a[1], false)
|
|
w, ok := g.windows[s]
|
|
if !ok {
|
|
return exception("'window-close' was given a window-id that does not exist: " + s)
|
|
}
|
|
w.Close()
|
|
return true
|
|
},
|
|
"window-set-title": func(a ...expression) expression {
|
|
if len(a) < 3 {
|
|
return exception("'window-set-title' expects a GUI and two strings; too few arguments were given")
|
|
}
|
|
g, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'window-set-title' was given a non-GUI value as its first argument")
|
|
}
|
|
s := String(a[1], false)
|
|
w, ok := g.windows[s]
|
|
if !ok {
|
|
return exception("'window-set-title' was given a window-id that does not exist: " + s)
|
|
}
|
|
w.SetTitle(String(a[2], false))
|
|
return true
|
|
},
|
|
"window-set-fullscreen": func(a ...expression) expression {
|
|
if len(a) < 3 {
|
|
return exception("'window-set-fullscreen' expects a GUI, a string, and a bool; too few arguments were given")
|
|
}
|
|
g, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'window-set-fullscreen' was given a non-GUI value as its first argument")
|
|
}
|
|
s := String(a[1], false)
|
|
w, ok := g.windows[s]
|
|
if !ok {
|
|
return exception("'window-set-fullscreen' was given a window-id that does not exist: " + s)
|
|
}
|
|
w.SetFullScreen(AnythingToBool(a[2]).(bool))
|
|
return true
|
|
},
|
|
"window-show-and-run": func(a ...expression) expression {
|
|
// (window-show-and-run window-id)
|
|
if len(a) < 2 {
|
|
return exception("'window-show-and-run' expects a GUI and a string as arguments, too few arguments were given")
|
|
}
|
|
g, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'window-show-and-run' was given a non-GUI value as its first argument")
|
|
}
|
|
s := String(a[1], false)
|
|
w, ok := g.windows[s]
|
|
if !ok {
|
|
return exception("'window-show-and-run' was given a window-id that does not exist: " + s)
|
|
}
|
|
w.ShowAndRun()
|
|
return true
|
|
},
|
|
"window-center": func(a ...expression) expression {
|
|
if len(a) < 2 {
|
|
return exception("'window-center' expects a GUI and a string as arguments, too few arguments were given")
|
|
}
|
|
g, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'window-center' was given a non-GUI value as its first argument")
|
|
}
|
|
s := String(a[1], false)
|
|
w, ok := g.windows[s]
|
|
if !ok {
|
|
return exception("'window-center' was given a window-id that does not exist: " + s)
|
|
}
|
|
w.CenterOnScreen()
|
|
return true
|
|
},
|
|
"window-allow-resize": func(a ...expression) expression {
|
|
if len(a) < 3 {
|
|
return exception("'window-allow-resize' expects a GUI, a string, and a bool; too few arguments were given")
|
|
}
|
|
g, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'window-allow-resize' was given a non-GUI value as its first argument")
|
|
}
|
|
s := String(a[1], false)
|
|
w, ok := g.windows[s]
|
|
if !ok {
|
|
return exception("'window-allow-resize' was given a window-id that does not exist: " + s)
|
|
}
|
|
w.SetFixedSize(!AnythingToBool(a[2]).(bool))
|
|
return true
|
|
},
|
|
"widget-disable": func(a ...expression) expression {
|
|
if len(a) == 0 {
|
|
return exception("'widget-disable' expects a gui-widget and an optional bool, no value was given")
|
|
}
|
|
wid, ok := a[0].(guiWidget)
|
|
if !ok {
|
|
return exception("'widget-disable' expects a gui-widget, a non gui-widget value was given")
|
|
|
|
}
|
|
switch w := wid.(fyne.Disableable).(type) {
|
|
case *widget.Entry, *widget.Button, *widget.Select, *widget.Check:
|
|
if len(a) == 1 {
|
|
return w.Disabled()
|
|
} else {
|
|
b := AnythingToBool(a[1]).(bool)
|
|
if b {
|
|
w.Disable()
|
|
} else {
|
|
w.Enable()
|
|
}
|
|
}
|
|
default:
|
|
fmt.Printf("%T - %v", w) // TODO: remove this line
|
|
return false
|
|
}
|
|
return true
|
|
},
|
|
"widget-show": func(a ...expression) expression {
|
|
if len(a) == 0 {
|
|
return exception("'widget-show' expects a widget, no value was given")
|
|
}
|
|
wid, ok := a[0].(guiWidget)
|
|
if !ok {
|
|
return exception("'widget-show' expects a widget, a non-widget value was given")
|
|
}
|
|
wid.Show()
|
|
return true
|
|
},
|
|
"widget-hide": func(a ...expression) expression {
|
|
if len(a) == 0 {
|
|
return exception("'widget-hide' expects a widget, no value was given")
|
|
}
|
|
wid, ok := a[0].(guiWidget)
|
|
if !ok {
|
|
return exception("'widget-hide' expects a widget, a non-widget value was given")
|
|
}
|
|
wid.Hide()
|
|
return true
|
|
},
|
|
"widget-resize": func(a ...expression) expression {
|
|
if len(a) < 3 {
|
|
return exception("'widget-rsize' expects a widget and two numbers, insufficient values were given")
|
|
}
|
|
canvas := false
|
|
var wid2 fyne.CanvasObject
|
|
wid, ok := a[0].(guiWidget)
|
|
if !ok {
|
|
wid2, ok = a[0].(fyne.CanvasObject)
|
|
if !ok {
|
|
return exception("'widget-resize' expects a widget, a non-widget value was given")
|
|
}
|
|
canvas = true
|
|
}
|
|
width, ok := a[1].(number)
|
|
if !ok {
|
|
return exception("'widget-resize' expects a width number, a non-number value was given")
|
|
}
|
|
height, ok := a[2].(number)
|
|
if !ok {
|
|
return exception("'widget-resize' expects a height number, a non-number value was given")
|
|
}
|
|
if !canvas {
|
|
wid.Resize(fyne.NewSize(float32(width), float32(height)))
|
|
return guiWidget(wid)
|
|
}
|
|
wid2.Resize(fyne.NewSize(float32(width), float32(height)))
|
|
return wid2
|
|
},
|
|
"widget-size": func(a ...expression) expression {
|
|
if len(a) == 0 {
|
|
return exception("'widget-sze' expects a widget, no value was given")
|
|
}
|
|
wid, ok := a[0].(guiWidget)
|
|
if !ok {
|
|
return exception("'widget-size' expects a widget, a non-widget value was given")
|
|
}
|
|
s := wid.Size()
|
|
return []expression{number(s.Width), number(s.Height)}
|
|
},
|
|
"widget-add-to-size": func(a ...expression) expression {
|
|
if len(a) < 3 {
|
|
return exception("'widget-add-to-size' expects a widget and two numbers, insufficient values were given")
|
|
}
|
|
wid, ok := a[0].(guiWidget)
|
|
if !ok {
|
|
return exception("'widget-add-to-size' expects a widget, a non-widget value was given")
|
|
}
|
|
width, ok := a[1].(number)
|
|
if !ok {
|
|
return exception("'widget-add-to-size' expects a width number, a non-number value was given")
|
|
}
|
|
height, ok := a[2].(number)
|
|
if !ok {
|
|
return exception("'widget-add-to-size' expects a height number, a non-number value was given")
|
|
}
|
|
s := wid.Size()
|
|
wid.Resize(s.Add(fyne.NewSize(float32(width), float32(height))))
|
|
return guiWidget(wid)
|
|
},
|
|
"widget-make-entry": func(a ...expression) expression {
|
|
if len(a) == 0 {
|
|
return exception("'widget-make-entry' expects at least one argument, a string representing the text of the label, no arguments were given")
|
|
}
|
|
var e guiWidget = widget.NewEntry()
|
|
|
|
// Set placeholder
|
|
if len(a) > 0 {
|
|
e.(*widget.Entry).SetPlaceHolder(String(a[0], false))
|
|
}
|
|
|
|
// Set wrapping
|
|
if len(a) > 1 && AnythingToBool(a[1]).(bool) {
|
|
e.(*widget.Entry).Wrapping = fyne.TextWrapWord
|
|
} else {
|
|
e.(*widget.Entry).Wrapping = fyne.TextTruncate
|
|
}
|
|
|
|
// Handle validation
|
|
if len(a) > 2 {
|
|
cb, ok := a[2].(proc)
|
|
if !ok {
|
|
return exception("'widget-make-entry' expects a lambdaas its optional third argument, a non-lambda value was given")
|
|
}
|
|
e.(*widget.Entry).Validator = func(in string) error {
|
|
result := apply(cb, []expression{in}, "Entry widget validator")
|
|
switch t := result.(type) {
|
|
case string:
|
|
return fmt.Errorf(t)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return e
|
|
},
|
|
"widget-make-text-grid": func(a ...expression) expression {
|
|
// (widget-make-text-grid [[text: string]] [[show-line-nums: bool]] [[show-whitespace: bool]] [[tabwidth: number]])
|
|
if len(a) == 0 {
|
|
return guiWidget(widget.NewTextGrid())
|
|
}
|
|
te := widget.NewTextGridFromString(String(a[0], false))
|
|
if len(a) >= 4 {
|
|
n, ok := a[3].(number)
|
|
if !ok {
|
|
return exception("'widget-make-text-grid' expected a number as its fourth argument, a non-number value was given")
|
|
}
|
|
te.TabWidth = int(n)
|
|
}
|
|
|
|
if len(a) >= 3 {
|
|
te.ShowWhitespace = AnythingToBool(a[2]).(bool)
|
|
}
|
|
|
|
if len(a) >= 3 {
|
|
te.ShowLineNumbers = AnythingToBool(a[1]).(bool)
|
|
}
|
|
return guiWidget(te)
|
|
},
|
|
"widget-make-password": func(a ...expression) expression {
|
|
if len(a) == 0 {
|
|
return exception("'widget-make-password' expects at least one argument, a string representing the text of the label, no arguments were given")
|
|
}
|
|
var e guiWidget = widget.NewPasswordEntry()
|
|
|
|
// Set placeholder
|
|
if len(a) > 0 {
|
|
e.(*widget.Entry).SetPlaceHolder(String(a[0], false))
|
|
}
|
|
|
|
// Set wrapping
|
|
if len(a) > 1 && AnythingToBool(a[1]).(bool) {
|
|
e.(*widget.Entry).Wrapping = fyne.TextWrapWord
|
|
} else {
|
|
e.(*widget.Entry).Wrapping = fyne.TextTruncate
|
|
}
|
|
|
|
// Handle validation
|
|
if len(a) > 2 {
|
|
cb, ok := a[2].(proc)
|
|
if !ok {
|
|
return exception("'widget-make-entry' expects a callback as its optional third argument, a non-callback value was given")
|
|
}
|
|
e.(*widget.Entry).Validator = func(in string) error {
|
|
result := apply(cb, []expression{in}, "Password widget validator")
|
|
switch t := result.(type) {
|
|
case string:
|
|
return fmt.Errorf(t)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return e
|
|
},
|
|
"widget-make-multiline": func(a ...expression) expression {
|
|
if len(a) == 0 {
|
|
return exception("'widget-make-multiline' expects at least one argument, a string representing the text of the label, no arguments were given")
|
|
}
|
|
var e guiWidget = widget.NewMultiLineEntry()
|
|
|
|
// Set placeholder
|
|
if len(a) > 0 {
|
|
e.(*widget.Entry).SetPlaceHolder(String(a[0], false))
|
|
}
|
|
|
|
// Set wrapping
|
|
if len(a) > 1 && AnythingToBool(a[1]).(bool) {
|
|
e.(*widget.Entry).Wrapping = fyne.TextWrapWord
|
|
} else {
|
|
e.(*widget.Entry).Wrapping = fyne.TextTruncate
|
|
}
|
|
|
|
if len(a) > 2 {
|
|
b := AnythingToBool(a[2]).(bool)
|
|
if b {
|
|
e.(*widget.Entry).TextStyle = fyne.TextStyle{false, false, true, 4}
|
|
}
|
|
}
|
|
|
|
// Handle validation
|
|
if len(a) > 3 {
|
|
cb, ok := a[3].(proc)
|
|
if !ok {
|
|
return exception("'widget-make-entry' expects a callback as its optional fourth argument, a non-callback value was given")
|
|
}
|
|
e.(*widget.Entry).Validator = func(in string) error {
|
|
result := apply(cb, []expression{in}, "Multiline widget validator")
|
|
switch t := result.(type) {
|
|
case string:
|
|
return fmt.Errorf(t)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return e
|
|
},
|
|
"widget-make-label": func(a ...expression) expression {
|
|
// (widget-label text:string alignment:number wrap:bool)
|
|
if len(a) == 0 {
|
|
return exception("'widget-make-label' expects at least one argument, a string representing the text of the label, no arguments were given")
|
|
}
|
|
var l guiWidget = widget.NewLabel(String(a[0], false))
|
|
if len(a) >= 2 {
|
|
align, ok := a[1].(number)
|
|
if !ok {
|
|
return exception("'widget-make-label' expects a number (-1, 0, or 1) as its optional second argument (alignment), a non-number value was given")
|
|
}
|
|
switch int(align) {
|
|
case -1:
|
|
l.(*widget.Label).Alignment = fyne.TextAlignLeading
|
|
case 0:
|
|
l.(*widget.Label).Alignment = fyne.TextAlignCenter
|
|
case 1:
|
|
l.(*widget.Label).Alignment = fyne.TextAlignTrailing
|
|
default:
|
|
return exception("'widget-make-label' expects a number (-1, 0, or 1) as its optional second argument (alignment), a number outside of the range was given")
|
|
}
|
|
}
|
|
if len(a) >= 3 {
|
|
wrap := AnythingToBool(a[0]).(bool)
|
|
if wrap {
|
|
l.(*widget.Label).Wrapping = fyne.TextWrapWord
|
|
}
|
|
}
|
|
return l
|
|
},
|
|
"widget-make-select": func(a ...expression) expression {
|
|
if len(a) < 2 {
|
|
return exception("'widget-make-select' expects a list of options as strings, a callback (lambda), and an optional alignment number too few arguments were given")
|
|
}
|
|
elist, ok := a[0].([]expression)
|
|
if !ok {
|
|
return exception("'widget-make-select' expected a list as its first argument, but was given a non-list value")
|
|
}
|
|
|
|
list := ExpressionSliceToStringSlice(elist)
|
|
|
|
var b guiWidget
|
|
switch f := a[1].(type) {
|
|
case func(...expression) expression:
|
|
b = widget.NewSelect(list, func(in string) { f(in) })
|
|
case proc:
|
|
b = widget.NewSelect(list, func(in string) { apply(f, []expression{in}, "Select widget callback") })
|
|
default:
|
|
return exception("'widget-make-select' was given a non-procedure value as its second argument")
|
|
}
|
|
if len(a) >= 3 {
|
|
align, ok := a[2].(number)
|
|
if !ok {
|
|
return exception("'widget-make-select' expects a number (-1, 0, or 1) as its optional third argument (alignment), a non-number value was given")
|
|
}
|
|
switch int(align) {
|
|
case -1:
|
|
b.(*widget.Select).Alignment = fyne.TextAlignLeading
|
|
case 0:
|
|
b.(*widget.Select).Alignment = fyne.TextAlignCenter
|
|
case 1:
|
|
b.(*widget.Select).Alignment = fyne.TextAlignTrailing
|
|
default:
|
|
return exception("'widget-make-select' expects a number (-1, 0, or 1) as its optional third argument (alignment), a number outside of the range was given")
|
|
}
|
|
}
|
|
return b
|
|
|
|
},
|
|
"widget-make-markdown": func(a ...expression) expression {
|
|
if len(a) == 0 {
|
|
return exception("'widget-make-markdown' expects a string but was given no arguments")
|
|
}
|
|
var b guiWidget
|
|
b = widget.NewRichTextFromMarkdown(String(a[0], false))
|
|
|
|
if len(a) >= 2 {
|
|
wrap := AnythingToBool(a[1]).(bool)
|
|
if wrap {
|
|
b.(*widget.RichText).Wrapping = fyne.TextWrapWord
|
|
}
|
|
}
|
|
return b
|
|
},
|
|
"widget-make-image": func(a ...expression) expression {
|
|
if len(a) < 2 {
|
|
return exception("'widget-make-image' expects two strings, with an optional string and two numbers, too few arguments were given")
|
|
}
|
|
fillMode := canvas.ImageFillOriginal
|
|
if len(a) > 2 {
|
|
if String(a[2], false) == "fit" {
|
|
fillMode = canvas.ImageFillContain
|
|
}
|
|
}
|
|
minWidth := float32(20)
|
|
minHeight := float32(20)
|
|
if len(a) > 3 {
|
|
w, ok := a[3].(number)
|
|
if ok {
|
|
minWidth = float32(w)
|
|
}
|
|
}
|
|
|
|
if len(a) > 4 {
|
|
h, ok := a[4].(number)
|
|
if ok {
|
|
minHeight = float32(h)
|
|
}
|
|
}
|
|
switch String(a[0], false) {
|
|
case "path":
|
|
i := canvas.NewImageFromFile(String(a[1], false))
|
|
i.FillMode = fillMode
|
|
i.SetMinSize(fyne.NewSize(minWidth, minHeight))
|
|
return i
|
|
case "url":
|
|
u, err := storage.ParseURI(String(a[1], false))
|
|
if err != nil {
|
|
return exception("'widget-make-image' was given 'url' value that could not be parsed to a valid url")
|
|
}
|
|
i := canvas.NewImageFromURI(u)
|
|
i.FillMode = fillMode
|
|
i.SetMinSize(fyne.NewSize(minWidth, minHeight))
|
|
return i
|
|
default:
|
|
return exception("'widget-make-image' was given an unsupported source string; 'path' and 'url' are the vailable options")
|
|
}
|
|
},
|
|
"widget-make-checkbox": func(a ...expression) expression {
|
|
if len(a) < 2 {
|
|
return exception("'widget-make-checkbox' expects a text string and a callback (lambda), too few arguments were given")
|
|
}
|
|
var b guiWidget
|
|
switch f := a[1].(type) {
|
|
case func(...expression) expression:
|
|
b = widget.NewCheck(String(a[0], false), func(in bool) { f(in) })
|
|
case proc:
|
|
b = widget.NewCheck(String(a[0], false), func(in bool) { apply(f, []expression{in}, "Checkbox widget callback") })
|
|
default:
|
|
return exception("'widget-make-checkbox' was given a non-procedure value as its second argument")
|
|
}
|
|
return b
|
|
},
|
|
"widget-make-hyperlink": func(a ...expression) expression {
|
|
if len(a) < 2 {
|
|
return exception("'widget-make-hyperlink' expects a text string and a URL string, too few arguments were given")
|
|
}
|
|
u, err := url.Parse(String(a[1], false))
|
|
if err != nil {
|
|
return exception("'widget-make-hyperlink' expects a valid URL as its second argument, could not parse URL from the given string")
|
|
}
|
|
return guiWidget(widget.NewHyperlink(String(a[0], false), u))
|
|
},
|
|
"widget-make-button": func(a ...expression) expression {
|
|
if len(a) < 2 {
|
|
return exception("'widget-make-button' expects at least two argument, a string representing the text of the label and a lambda, too few arguments were given")
|
|
}
|
|
var b guiWidget
|
|
switch f := a[1].(type) {
|
|
case func(...expression) expression:
|
|
b = widget.NewButton(String(a[0], false), func() { f() })
|
|
case proc:
|
|
b = widget.NewButton(String(a[0], false), func() { apply(f, []expression{}, "Button widget callback") })
|
|
default:
|
|
return exception("'widget-make-button' was given a non-procedure value as its second argument")
|
|
}
|
|
if len(a) >= 3 {
|
|
align, ok := a[2].(number)
|
|
if !ok {
|
|
return exception("'widget-make-button' expects a number (-1, 0, or 1) as its optional third argument (alignment), a non-number value was given")
|
|
}
|
|
switch int(align) {
|
|
case -1:
|
|
b.(*widget.Button).Alignment = widget.ButtonAlignLeading
|
|
case 0:
|
|
b.(*widget.Button).Alignment = widget.ButtonAlignCenter
|
|
case 1:
|
|
b.(*widget.Button).Alignment = widget.ButtonAlignTrailing
|
|
default:
|
|
return exception("'widget-make-button' expects a number (-1, 0, or 1) as its optional third argument (alignment), a number outside of the range was given")
|
|
}
|
|
}
|
|
if len(a) >= 4 {
|
|
im, ok := a[3].(number)
|
|
if !ok {
|
|
return exception("'widget-make-button' was given a non-number value for 'importance'")
|
|
}
|
|
importance := int(im)
|
|
switch true {
|
|
case (importance < 1):
|
|
b.(*widget.Button).Importance = widget.LowImportance
|
|
case (importance > 1):
|
|
b.(*widget.Button).Importance = widget.HighImportance
|
|
default:
|
|
b.(*widget.Button).Importance = widget.MediumImportance
|
|
}
|
|
}
|
|
return b
|
|
},
|
|
"widget-get-text": func(a ...expression) expression {
|
|
if len(a) < 1 {
|
|
return exception("'widget-get-text' expects a widget argument, no arguments were given")
|
|
}
|
|
w, ok := a[0].(guiWidget)
|
|
if !ok {
|
|
return exception("'widget-get-text' expects a widget as its first argument, a non-widget value was given")
|
|
}
|
|
switch wid := w.(fyne.Widget).(type) {
|
|
case *widget.Label:
|
|
return wid.Text
|
|
case *widget.TextGrid:
|
|
return wid.Text()
|
|
case *widget.Button:
|
|
return wid.Text
|
|
case *widget.Entry:
|
|
return wid.Text
|
|
case *widget.Hyperlink:
|
|
return wid.Text
|
|
case *widget.Check:
|
|
return wid.Text
|
|
case *widget.RichText:
|
|
return wid.String()
|
|
case *widget.Select:
|
|
return wid.Selected
|
|
default:
|
|
return exception("'widget-get-text' received a widget that it does not support")
|
|
}
|
|
},
|
|
"widget-set-text": func(a ...expression) expression {
|
|
if len(a) < 2 {
|
|
return exception("'widget-set-text' expects two arguments, a widget and a string representing the text of the label, no arguments were given")
|
|
}
|
|
w, ok := a[0].(guiWidget)
|
|
if !ok {
|
|
return exception("'widget-set-text' expects a widget as its first argument, a non-widget value was given")
|
|
}
|
|
txt := String(a[1], false)
|
|
switch wid := w.(fyne.Widget).(type) {
|
|
case *widget.Label:
|
|
wid.SetText(txt)
|
|
case *widget.TextGrid:
|
|
wid.SetText(txt)
|
|
case *widget.Button:
|
|
wid.SetText(txt)
|
|
case *widget.Entry:
|
|
wid.SetText(txt)
|
|
case *widget.Hyperlink:
|
|
wid.SetText(txt)
|
|
case *widget.Check:
|
|
wid.Text = txt
|
|
wid.Refresh()
|
|
case *widget.RichText:
|
|
wid.ParseMarkdown(txt)
|
|
case *widget.Select:
|
|
wid.SetSelected(txt)
|
|
default:
|
|
return exception("'widget-set-text' does not support setting the text of the given value")
|
|
}
|
|
return txt
|
|
},
|
|
"widget-make-separator": func(a ...expression) expression {
|
|
return guiWidget(widget.NewSeparator())
|
|
},
|
|
"widget-make-spacer": func(a ...expression) expression {
|
|
var spacer guiObject = layout.NewSpacer()
|
|
return spacer
|
|
},
|
|
"container-scroll": func(a ...expression) expression {
|
|
if len(a) < 1 {
|
|
return exception("'container-scroll' expects at least one widget or layout, no values were given")
|
|
}
|
|
list, err := ExpToCanvasObjectList(a)
|
|
if err != nil || len(list) < 1 {
|
|
return exception("'container-scroll' was given a non-widget value")
|
|
}
|
|
return guiContainer(container.NewScroll(list[0]))
|
|
},
|
|
"container": func(a ...expression) expression {
|
|
if len(a) < 2 {
|
|
return exception("'container' expects at least two arguments, a string representing the layout type and at least one widget or layout (or in some layout types an additional value), no values were given")
|
|
}
|
|
containerType := String(a[0], false)
|
|
widgetStart := 1
|
|
cols := 0
|
|
if containerType == "grid" {
|
|
if len(a) < 3 {
|
|
return exception("'container' with layout 'grid' expects at least three arguments, a string representing the layout type, a number representing the column count, and at least one widget or layout, no values were given")
|
|
}
|
|
if count, ok := a[1].(number); ok {
|
|
widgetStart = 2
|
|
cols = int(float64(count))
|
|
}
|
|
}
|
|
list, err := ExpToCanvasObjectList(a[widgetStart:])
|
|
if err != nil {
|
|
return exception("'container' was given a non-widget value")
|
|
}
|
|
switch containerType {
|
|
case "none":
|
|
return guiContainer(container.NewWithoutLayout(list...))
|
|
case "hbox":
|
|
return guiContainer(container.NewHBox(list...))
|
|
case "vbox":
|
|
return guiContainer(container.NewVBox(list...))
|
|
case "form":
|
|
return guiContainer(container.New(layout.NewFormLayout(), list...))
|
|
case "center":
|
|
return guiContainer(container.NewCenter(list...))
|
|
case "max":
|
|
return guiContainer(container.NewMax(list...))
|
|
case "padded":
|
|
return guiContainer(container.NewPadded(list...))
|
|
case "grid":
|
|
return guiContainer(container.NewGridWithColumns(cols, list...))
|
|
case "vsplit":
|
|
if len(list) < 2 {
|
|
return exception("'container' of type 'vsplit' expects three arguments, a string representing the layout type ('vsplit') and two widgets or layouts, too few arguments were given")
|
|
}
|
|
return guiContainer(container.NewVSplit(list[0], list[1]))
|
|
case "hsplit":
|
|
if len(list) < 2 {
|
|
return exception("'container' of type 'hsplit' expects three arguments, a string representing the layout type ('hsplit') and two widgets or layouts, too few arguments were given")
|
|
}
|
|
return guiContainer(container.NewHSplit(list[0], list[1]))
|
|
case "border":
|
|
if len(a) < 4 {
|
|
return exception("'container' expects at least four elements when set to 'border', too few elements were given")
|
|
} else if len(a) == 4 {
|
|
return guiContainer(container.NewBorder(list[0], list[1], list[2], list[3]))
|
|
} else {
|
|
return guiContainer(container.NewBorder(list[0], list[1], list[2], list[3], list[4:]...))
|
|
}
|
|
default:
|
|
return exception("'container-with-layout' was given an invalid layout type")
|
|
}
|
|
},
|
|
"container-size": func(a ...expression) expression {
|
|
if len(a) < 1 {
|
|
return exception("'container-size' expects a container, no value was given")
|
|
}
|
|
c, ok := a[0].(guiContainer)
|
|
if !ok {
|
|
return exception("'container-size' expects a container, a non-container value was given")
|
|
}
|
|
s := c.Size()
|
|
return []expression{number(s.Width), number(s.Height)}
|
|
},
|
|
"container-add-to": func(a ...expression) expression {
|
|
if len(a) < 2 {
|
|
return exception("'container-add-to' expects a container and either a widget or another container, insufficient values were given")
|
|
}
|
|
c, ok := a[0].(guiContainer)
|
|
if !ok {
|
|
return exception("'container-add-to' expects a container, a non-container value was given")
|
|
}
|
|
switch obj := a[1].(type) {
|
|
case guiContainer:
|
|
c.(*fyne.Container).Add(obj)
|
|
case guiWidget:
|
|
c.(*fyne.Container).Add(obj)
|
|
default:
|
|
return exception("'container-add-to' expects a container or a widget as its second argument, a non-widget/non-container value was given")
|
|
}
|
|
return true
|
|
},
|
|
"dialog-open-file": func(a ...expression) expression {
|
|
if len(a) < 3 {
|
|
return exception("'dialog-open-file' expected three arguments: a GUI, a string representing a gui-window-id and a callback that takes a string as an argument; an insufficient number of arguments was given")
|
|
}
|
|
app, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'dialog-open-file' expected a GUI as its first argument, a non-GUI value was given")
|
|
}
|
|
window := String(a[1], false)
|
|
if _, ok := app.windows[window]; !ok {
|
|
return exception("'dialog-open-file' was given an invalid window-id: " + window)
|
|
}
|
|
cb, ok := a[2].(proc)
|
|
if !ok {
|
|
return exception("'dialog-open-file' expected a callback as its second argument, a non-callback value was given")
|
|
}
|
|
fd := dialog.NewFileOpen(func(reader fyne.URIReadCloser, err error) {
|
|
if err != nil || reader == nil {
|
|
return
|
|
}
|
|
defer reader.Close()
|
|
u := reader.URI().Path()
|
|
apply(cb, []expression{u}, "Open file dialog callback")
|
|
return
|
|
}, app.windows[window])
|
|
fd.Show()
|
|
return []expression{}
|
|
},
|
|
"dialog-save-file": func(a ...expression) expression {
|
|
if len(a) < 3 {
|
|
return exception("'dialog-save-file' expected three arguments: a GUI, a string representing a gui-window-id and a callback that takes a string as an argument; an insufficient number of arguments was given")
|
|
}
|
|
app, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'dialog-save-file' expected a GUI as its first argument, a non-GUI value was given")
|
|
}
|
|
window := String(a[1], false)
|
|
if _, ok := app.windows[window]; !ok {
|
|
return exception("'dialog-save-file' was given an invalid window-id: " + window)
|
|
}
|
|
cb, ok := a[2].(proc)
|
|
if !ok {
|
|
return exception("'dialog-save-file' expected a callback as its second argument, a non-callback value was given")
|
|
}
|
|
fd := dialog.NewFileSave(func(writer fyne.URIWriteCloser, err error) {
|
|
if err != nil || writer == nil {
|
|
return
|
|
}
|
|
defer writer.Close()
|
|
u := writer.URI().Path()
|
|
apply(cb, []expression{u}, "Save file dialog callback")
|
|
return
|
|
}, app.windows[window])
|
|
fd.Show()
|
|
return []expression{}
|
|
},
|
|
"dialog-info": func(a ...expression) expression {
|
|
if len(a) < 4 {
|
|
return exception("'dialog-info' expected four arguments: a GUI, a string representing a window-name, a title string, and a message string; an insufficient number of arguments was given")
|
|
}
|
|
app, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'dialog-info' expected a GUI as its first argument, a non-GUI value was given")
|
|
}
|
|
window := String(a[1], false)
|
|
if _, ok := app.windows[window]; !ok {
|
|
return exception("'dialog-info' was given an invalid window-id: " + window)
|
|
}
|
|
dialog.ShowInformation(String(a[2], false), String(a[3], false), app.windows[window])
|
|
return []expression{}
|
|
},
|
|
"dialog-error": func(a ...expression) expression {
|
|
if len(a) < 3 {
|
|
return exception("'dialog-error' expected three arguments: a GUI, a string representing a gui-window-id, an error string; an insufficient number of arguments was given")
|
|
}
|
|
app, ok := a[0].(*GUI)
|
|
if !ok {
|
|
return exception("'dialog-error' expected a GUI as its first argument, a non-GUI value was given")
|
|
}
|
|
window := String(a[1], false)
|
|
if _, ok := app.windows[window]; !ok {
|
|
return exception("'dialog-error' was given an invalid window-id: " + window)
|
|
}
|
|
dialog.ShowError(fmt.Errorf(String(a[2], false)), app.windows[window])
|
|
return []expression{}
|
|
},
|
|
}
|
|
|
|
// - BEGIN helpers
|
|
|
|
func ExpToCanvasObjectList(e []expression) ([]fyne.CanvasObject, error) {
|
|
out := make([]fyne.CanvasObject, len(e))
|
|
for i, v := range e {
|
|
switch obj := v.(type) {
|
|
case bool:
|
|
out[i] = nil
|
|
case guiWidget:
|
|
out[i] = obj.(fyne.Widget).(fyne.CanvasObject)
|
|
case fyne.CanvasObject:
|
|
out[i] = obj
|
|
case *fyne.Container:
|
|
out[i] = obj
|
|
case guiObject:
|
|
out[i] = obj.(fyne.CanvasObject)
|
|
case guiContainer:
|
|
out[i] = obj.(fyne.CanvasObject)
|
|
default:
|
|
return out, fmt.Errorf("Invalid list")
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// String converts any value to a string
|
|
// if rawString is true escapes will be
|
|
// shown visually and quotes will be around
|
|
// a string. rawString has no effect on
|
|
// values that are not already strings
|
|
func String(v expression, rawString bool) string {
|
|
switch v := v.(type) {
|
|
case []expression:
|
|
l := make([]string, len(v))
|
|
for i, x := range v {
|
|
l[i] = String(x, true)
|
|
}
|
|
return "(" + strings.Join(l, " ") + ")"
|
|
case string:
|
|
if rawString {
|
|
return fmt.Sprintf("\"%s\"", escapeString(v))
|
|
}
|
|
return v
|
|
case exception:
|
|
return string(v)
|
|
case bool:
|
|
if v {
|
|
return "#t"
|
|
}
|
|
return "#f"
|
|
case number:
|
|
return strconv.FormatFloat(float64(v), 'f', -1, 64)
|
|
case proc:
|
|
var b strings.Builder
|
|
b.WriteString("(lambda ")
|
|
b.WriteString(String(v.params, true))
|
|
b.WriteRune(' ')
|
|
body := String(v.body, true)
|
|
if strings.HasPrefix(body, "(begin ") {
|
|
body = body[7:]
|
|
}
|
|
b.WriteString(body)
|
|
return b.String()
|
|
case func(...expression) expression:
|
|
return fmt.Sprint("Built-in: ", &v)
|
|
case *IOHandle:
|
|
return fmt.Sprintf("%+v", v)
|
|
case guiWidget:
|
|
return "gui-widget"
|
|
case fyne.Widget:
|
|
return "gui-widget"
|
|
case canvas.Image:
|
|
return "gui-widget"
|
|
case *canvas.Image:
|
|
return "gui-widget"
|
|
case fyne.Container:
|
|
return "gui-container"
|
|
case guiContainer:
|
|
return "gui-container"
|
|
case fyne.CanvasObject:
|
|
return "gui-widget"
|
|
case GUI:
|
|
return "gui-root"
|
|
case *GUI:
|
|
return "gui-root"
|
|
default:
|
|
return fmt.Sprint(v)
|
|
}
|
|
}
|