Open andlabs opened 6 years ago
One functionality that would make this a lot easier would be a capability to create things without having to use the built in "NewWhatever" functions.
Two APIs I think would work:
A "create" function which you can hand a struct pointer to an UI element struct you already created using the new() builtin, that "gives life" to that struct.
btn := new(ui.Button)
ui.Create(btn)
This would mean a declarative approach to first creating the controls would be possible, then to give them life, the API of ui is used to populate the control internally.
Second approach that would be externally programmable without changing ui is to create a struct type that can be any control by the means of using a "Type" attribute. This struct type would then be easily serializeable even in JSON or YAML. A functionality inside ui or even externally would iterate through this structure recursively (child elements, etc.) and create the controls, then hand back the root element.
Though I don't really have a good idea on how to map events in all these cases. I think a system to find controls by ID would fix this, but it would be a major change in ui
@sinni800 @andlabs I have created a very simple example of declaratively creating the UI, including basic event handling.
Thoughts? Do you think this is a feasible way to go about creating a declarative api?
Some caveats to note (that could probably be rectified with more thought):
Init()
.github.com/andlabs/ui
type needs to be wrapped in another type that can initialise it declaritively. Store
object must be within the scope of the closures so that it can be referenced.package main
import (
"log"
"github.com/andlabs/ui"
)
func main() {
if err := ui.Main(run); err != nil {
log.Fatalf("error: %v", err)
}
}
func run() {
store := &Store{}
app := &Window{
Title: "Reverse Text",
W: 800,
H: 600,
Margined: true,
OnClosing: func(w *ui.Window) bool {
ui.Quit()
return true
},
Child: &Form{
Padded: true,
Controls: []FormControl{
FormControl{
Label: "Input",
Control: &Entry{
OnChanged: func(e *ui.Entry) {
store.Push(Event{
Name: "input-changed",
Value: e.Text(),
})
},
},
},
FormControl{
Label: "Reversed",
Control: &Entry{
ReadOnly: true,
OnEvent: func(entry *Entry, event Event) {
switch event.Name {
case "input-changed":
if text, ok := event.Value.(string); ok {
entry.SetText(reverse(text))
}
}
},
},
},
},
},
}
if err := app.Init(store); err != nil {
log.Fatalf("initialising: %v", err)
}
app.Show()
}
// reverse a string.
func reverse(s string) string {
chars := []rune(s)
for i, j := 0, len(chars)-1; i < j; i, j = i+1, j-1 {
chars[i], chars[j] = chars[j], chars[i]
}
return string(chars)
}
// Event wraps some named data.
type Event struct {
Name string
Value interface{}
}
// Receiver is any object that can receive events.
type Receiver interface {
Receive(Event)
}
// Store pushes events to attached receivers.
type Store struct {
Receivers []Receiver
}
// Push a single event to all attached receivers.
func (s *Store) Push(e Event) {
for _, r := range s.Receivers {
r.Receive(e)
}
}
// Attach a receiver to the store.
func (s *Store) Attach(r Receiver) {
s.Receivers = append(s.Receivers, r)
}
// Control is any ui.Control that can initialise itself.
type Control interface {
ui.Control
Init(*Store) error
}
// Window initialises a ui.Window.
type Window struct {
*ui.Window
Title string
W, H int
Menu bool
Margined bool
Child Control
OnClosing func(*ui.Window) bool
}
// Init builds the Window.
func (w *Window) Init(s *Store) error {
w.Window = ui.NewWindow(w.Title, w.W, w.H, w.Menu)
w.Window.SetMargined(w.Margined)
w.Window.OnClosing(w.OnClosing)
if err := w.Child.Init(s); err != nil {
return err
}
w.Window.SetChild(w.Child)
return nil
}
// Entry initialises a ui.Entry.
type Entry struct {
*ui.Entry
Placeholder string
ReadOnly bool
OnChanged func(*ui.Entry)
OnEvent func(*Entry, Event)
}
// Init builds the Entry.
func (e *Entry) Init(s *Store) error {
e.Entry = ui.NewEntry()
e.Entry.SetText(e.Placeholder)
e.Entry.SetReadOnly(e.ReadOnly)
e.Entry.OnChanged(e.OnChanged)
s.Attach(e)
return nil
}
// Receive an event.
func (e *Entry) Receive(event Event) {
if e.OnEvent == nil {
return
}
e.OnEvent(e, event)
}
// Form initialises a ui.Form.
type Form struct {
*ui.Form
Padded bool
Controls []FormControl
}
// Init builds the Form.
func (f *Form) Init(s *Store) error {
f.Form = ui.NewForm()
f.Form.SetPadded(f.Padded)
for _, c := range f.Controls {
if err := c.Control.Init(s); err != nil {
return err
}
f.Form.Append(c.Label, c.Control, c.Stretchy)
}
return nil
}
// FormControl pairs a control with a label.
type FormControl struct {
Label string
Stretchy bool
Control Control
}
It's easier to build one of these on top of an existing system than it is to build one of the existing systems on top of one of these, so this would definitely sit on top of package ui, rather than replace it entirely.
That being said, I don't want a One True Standard for this, or at the least, I don't have any particular feeling in favor of one over the other. I'm also not sure if such a thing should be Go-specific or not. The primary goal of this issue is to have one single place for this discussion.