andlabs / ui

Platform-native GUI library for Go.
Other
8.34k stars 649 forks source link

Master issue for a declarative, reactive, data-driven, or whatever else you want to call it syntax for Go #276

Open andlabs opened 6 years ago

andlabs commented 6 years ago

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.

sinni800 commented 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

JackMordaunt commented 6 years ago

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

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
}