-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Typed Channel Bindings
Note
This document is archived as it refers to a design process.
This proposal was not implemented instead going for the data/binding
package that is in Fyne v2.
When the data changes, update the UI and when the UI changes, update the data.
Settings, Theme, and Widgets have 'properties' which are exported binding fields. A goroutine can call listen on these properties and receive a typed channel from which updates can be read. When a binding changes, the update is written to all channels.
- Reflection - set function fields in widget.
- Concurrent Notification - each binding listener is notified in a newly created goroutine.
- Updater Thread - use a single looper to ensure updates are ordered.
This proposal differs from the previous by removing the binding updater thread which iterated through changes and notifying the listeners. Instead each listener is a goroutine reading from a channel, when the binding changes it sends the data on each channel. This approach guarantees channels are written in the order they were registered, but does not guaranteed the listener executes in order. This approach ensures that renderer objects are only updated by a single thread and therefore don't require synchronization.
// Binding is the base interface of the Data Binding API.
type Binding interface {
// Update notifies all listeners after a change.
Update()
}
// Collection is a binding holding a collection of multiple bindings.
type Collection interface {
Binding
// Length returns the length of the collection.
Length() int
// Listen returns a channel through which collection updates will be published.
Listen() <-chan int
// OnUpdate calls the given function whenever the collection updates.
OnUpdate(func(int))
}
// List defines a data binding wrapping a list of bindings.
type List interface {
Collection
// Get returns the binding at the given index.
Get(int) Binding
}
// Map defines a data binding wrapping a list of bindings.
type Map interface {
Collection
// Get returns the binding for the given key.
Get(string) (Binding, bool)
}
Primitive Bindings;
- bool
- float64
- int
- int64
- rune
- string
- *url.URL
Fyne Bindings;
- fyne.Position
- fyne.Resource
- fyne.Size
type Bool interface {
Binding
Get() bool
GetRef() *bool
Set(bool)
SetRef(*bool)
Listen() <-chan bool
OnUpdate(func(bool))
}
func EmptyBool() Bool { ... }
func NewBool(value bool) Bool { ... }
func NewBoolRef(reference *bool) Bool { ... }
type BoolList interface {
List
GetBinding(int) Bool
GetBool(int) bool
GetRef(int) *bool
SetBinding(int, Bool)
SetBool(int, bool)
SetRef(int, *bool)
AddBinding(Bool)
AddBool(bool)
AddRef(*bool)
}
func NewBoolList(values ...bool) BoolList { ... }
func NewBoolListRefs(references *[]*bool) BoolList { ... }
A Binding holds a reference to the underlying data so it can either be changes through the Binding or elsewhere.
i := 5
ib := NewIntRef(&i)
ib.Set(6)
// Or
i = 6
ib.Update()
Deprecate WidgetRenderer.Refresh - instead Widget.CreateRenderer will start a goroutine that is stopped by WidgetRenderer.Destroy and will listen to each of the Widget's properties, perform the appropriate action when an update occurs, and call canvas.Refresh() to enqueue the widget for rendering. This means that as soon as a property is changed, the UI updates, however it has the downside that multiple properties changes results in the widget being enqueued for render multiple times.
Should CanvasObjects use Properties and CanvasObject.Refresh be deprecated too? - no because CanvasObjects don't have a renderer to listen to properties and trigger canvas.Refresh.
Idiomatic Go does not put a "Get" prefix on accessor methods of unexported fields Effective Go (Getters). However properties must be exported and thus should either be named weirdly, or the accessors must start with "Get".
For example, a Widget has either;
- a 'SizeProperty binding.Size' field and implements 'Size() fyne.Size' and 'SetSize(fyne.Size)'
- a 'Size binding.Size' field and implements 'GetSize() fyne.Size' and 'SetSize(fyne.Size)'
Button displays Text and/or an Image and can be Tapped.
type Button struct {
DisableableWidget
ExtendableWidget
HoverableWidget
HideShadow binding.Bool
Icon binding.Resource
Text binding.String
OnTapped func()
}
i := binding.NewResource(theme.InfoIcon())
t := binding.NewString("Foo")
w := widget.Button{
Icon: i,
Text: t,
OnTapped: func() {
log.Println("Tapped!")
},
}
go func() {
time.Sleep(5 * time.Second)
i.Set(theme.WarningIcon())
}()
go func() {
time.Sleep(10 * time.Second)
t.Set("Bar")
}()
Check is a specialization of Button which can be toggled.
type Check struct {
DisableableWidget
ExtendableWidget
FocusableWidget
HoverableWidget
Checked binding.Bool
Text binding.String
OnChanged func(bool)
}
t := binding.NewString("Foo")
w := widget.Check{
Text: t,
OnChanged: func(c bool) {
if c {
log.Println("Checked!")
} else {
log.Println("Unchecked!")
}
},
}
go func() {
time.Sleep(5 * time.Second)
t.Set("Bar")
}()
go func() {
time.Sleep(10 * time.Second)
w.Checked.Set(true)
}()
Entry is a specialization of Label which supports editing.
Form is a specialization of List which renders each item with a Label, and has function fields to Clear and Submit.
Group is a specialization of List which renders a title and visual border.
Hyperlink is a specialization of Label which opens a URL in the default browser when tapped.
Icon displays an image.
Label displays text.
List displays a collection of items.
type List struct {
ExtendableWidget
DisableableWidget
HoverableWidget
Horizontal binding.Bool
Items binding.List
Selected binding.Int
OnCreateCell func() fyne.CanvasObject
OnBindCell func(fyne.CanvasObject, binding.Binding)
OnSelected func(int)
indexFirst binding.Int
indexHovered binding.Int
indexLast binding.Int
offsetItem binding.Int
offsetScroll binding.Int
}
w := widget.NewList([]string{"1", "2", "3"}, func(index int) {
fmt.Printf("selected: %d\n", index)
})
// If OnCreateCell is nil, a Label will be used.
// If OnBindCell is nil, and the cell is a Label, its Text will be bound.
w := &widget.List{
Items: binding.NewStringList("https://fyne.io", "https://github.com/fyne-io"),
OnCreateCell: func() {
return &Hyperlink{}
},
OnBindCell: func(c fyne.CanvasObject, b binding.Binding) {
hl, ok := c.(*Hyperlink)
if ok {
s, ok := b.(binding.String)
if ok {
hl.Text = s
u, err := url.Parse(s)
if err != nil {
fyne.LogError(fmt.Sprintf("Could not parse URL: %s", s), err)
} else {
hl.URL = u
}
}
hl.Color = theme.TextColor.Get()
hl.TextSize = theme.TextSize.Get()
}
},
OnSelected: func(index int) {
fmt.Printf("selected: %d\n", index)
},
}
Menu is a specialization of Tree which renders with Popups.
Radio is a specialization of List which renders items with an icon and ensures only one element can be selected.
type Radio struct {
List
}
w := &widget.Radio{
Items: binding.NewStringList("A", "B", "C"),
OnSelected: func(index int) {
fmt.Printf("selected: %d\n", index)
},
}
Select is a specialization of List which renders the list in a popup when activated.
type Select struct {
List
}
w := &widget.Select{
Items: binding.NewStringList("A", "B", "C"),
OnSelected: func(index int) {
fmt.Printf("selected: %d\n", index)
},
}
Table is a specialization of List which renders each row as a List of columns.
Tree is a specialization of List which renders hierarchical data where each item is a List.
type MyWidget struct {
// Reuse existing Behaviour and Properties
widget.DisableableWidget // Implements Disableable and provides Disabled Property.
widget.ExtendableWidget // Enables custom Widgets to extend existing Widgets.
widget.FocusableWidget // Implements Focusable and provides Focused Property.
canvas.GeometricObject // Implements CanvasObject and provides Size, Position, and Hidden Properties.
desktop.HoverableWidget // Implements Hoverable and provides Hovered Property.
// Custom Behaviour and Properties
Mode binding.Int
OnModeChange func(int)
}
// NewMyWidget creates a new widget with the mode and change handler
func NewMyWidget(mode int, changed func()) *MyWidget {
w := &MyWidget{
Mode: binding.NewInt(mode),
OnModeChange: changed,
}
w.ExtendWidget(w)
return w
}
func (w *MyWidget) CreateRenderer() fyne.WidgetRenderer {
w.ExtendWidget(w)
// Ensure all Properties are set.
if w.Disabled == nil {
w.Disabled = binding.EmptyBool()
}
if w.Hidden == nil {
w.Hidden = binding.EmptyBool()
}
if w.Hovered == nil {
w.Hovered = binding.EmptyBool()
}
if w.Mode == nil {
w.Mode = binding.EmptyInt()
}
if w.Position == nil {
w.Position = binding.EmptyPosition()
}
if w.Size == nil {
w.Size = binding.EmptySize()
}
if theme.TextColor == nil {
theme.TextColor = binding.EmptyColor()
}
if theme.TextSize == nil {
theme.TextSize = binding.EmptyInt()
}
// Create objects.
label := &canvas.Text{}
// Create Renderer.
r := &myWidgetRenderer{
myWidget: w,
label: label,
done: make(chan bool),
}
// Listen to all Property Channels.
disabledChan := w.Disabled.Listen()
hiddenChan := w.Hidden.Listen()
hoveredChan := w.Hovered.Listen()
modeChan := w.Mode.Listen()
positionChan := w.Position.Listen()
sizeChan := w.Size.Listen()
themeTextColorChan := theme.TextColor.Listen()
themeTextSizeChan := theme.TextSize.Listen()
// Create goroutine to respond to changes, and trigger refresh.
go func() {
for {
select {
case d := <-disabledChan:
log.Println("disabled:", d)
case h := <-hiddenChan:
log.Println("hidden:", h)
case h := <-hoveredChan:
log.Println("hovered:", h)
case m := <-modeChan:
log.Println("mode:", m)
label.Text = fmt.Sprintf("Mode: %d", m) // Update Label when Mode Changes.
case p := <-positionChan:
log.Println("position:", p)
case s := <-sizeChan:
log.Println("size:", s)
case c := <-themeTextColorChan:
log.Println("textcolor:", c)
label.Color = c // Update Label when Theme Color Changes.
case s := <-themeTextSizeChan:
log.Println("textsize:", s)
label.TextSize = s // Update Label when Theme Text Size Changes.
case <-r.done:
return
}
// FIXME multiple updates can cause multiple layouts and renders.
r.Layout(b.CurrentSize())
canvas.Refresh(b.super())
}
}()
return r
}
func (w *MyWidget) MinSize() fyne.Size {
w.ExtendWidget(w)
return w.ExtendableWidget.MinSize()
}
func (w *MyWidget) Tapped(*fyne.PointEvent) {
if w.IsDisabled() {
return
}
mode.Set(mode.Get()+1)
if w.OnModeChange != nil {
w.OnModeChange(mode.Get())
}
}
type myWidgetRenderer struct {
myWidget *MyWidget
label *canvas.Text
done chan bool
}
func (r *myWidgetRenderer) BackgroundColor() color.Color {
if r.myWidget.IsHovered() {
return theme.HoverColor.Get()
}
return theme.PrimaryColor.Get()
}
func (r *myWidgetRenderer) Destroy() {
r.done <- true
}
func (r *myWidgetRenderer) Layout(size fyne.Size) {
// ...
}
func (r *myWidgetRenderer) MinSize() fyne.Size {
// ...
}
func (r *myWidgetRenderer) Objects() []CanvasObject {
return []fyne.CanvasObject{r.label}
}