Skip to content

Commit

Permalink
First pass for latest data binding API proposal
Browse files Browse the repository at this point in the history
This commit is float value to test concept.
Includes a simple demo :)
  • Loading branch information
andydotxyz committed Nov 6, 2020
1 parent 808a4b1 commit 7154e4c
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 1 deletion.
56 changes: 56 additions & 0 deletions binding/binding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package binding

// DataItemListener is any object that can register for changes in a bindable DataItem.
// See NewNotifyFunction to define a new listener using just an inline function.
type DataItemListener interface {
DataChanged(DataItem)
}

// DataItem is the base interface for all bindable data items.
type DataItem interface {
AddListener(DataItemListener)
RemoveListener(DataItemListener)
}

type base struct {
listeners []DataItemListener
}

// AddListener allows a data listener to be informed of changes to this item.
func (b *base) AddListener(l DataItemListener) {
b.listeners = append(b.listeners, l)
}

// RemoveListener should be called if the listener is no longer interested in being informed of data change events.
func (b *base) RemoveListener(l DataItemListener) {
for i, listen := range b.listeners {
if listen != l {
continue
}

if i == len(b.listeners)-1 {
b.listeners = b.listeners[:len(b.listeners)-1]
} else {
b.listeners = append(b.listeners[:i], b.listeners[i+1:]...)
}
}
}

func (b *base) trigger(i DataItem) {
for _, listen := range b.listeners {
listen.DataChanged(i)
}
}

type listener struct {
callback func(DataItem)
}

// NewNotifyFunction is a helper function that creates a new listener type from a simple callback function.
func NewNotifyFunction(fn func(DataItem)) DataItemListener {
return &listener{fn}
}

func (l *listener) DataChanged(i DataItem) {
l.callback(i)
}
52 changes: 52 additions & 0 deletions binding/binding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package binding

import (
"testing"

"github.com/stretchr/testify/assert"
)

type simpleItem struct {
base
}

func TestBase_AddListener(t *testing.T) {
data := &simpleItem{}
assert.Equal(t, 0, len(data.listeners))

called := false
fn := NewNotifyFunction(func(DataItem) {
called = true
})
data.AddListener(fn)
assert.Equal(t, 1, len(data.listeners))

data.trigger(data)
assert.True(t, called)
}

func TestBase_RemoveListener(t *testing.T) {
called := false
fn := NewNotifyFunction(func(DataItem) {
called = true
})
data := &simpleItem{}
data.listeners = []DataItemListener{fn}

assert.Equal(t, 1, len(data.listeners))
data.RemoveListener(fn)
assert.Equal(t, 0, len(data.listeners))

data.trigger(data)
assert.False(t, called)
}

func TestNewNotifyFunction(t *testing.T) {
called := false
fn := NewNotifyFunction(func(DataItem) {
called = true
})

fn.DataChanged(nil)
assert.True(t, called)
}
42 changes: 42 additions & 0 deletions binding/float.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package binding

// Float supports binding a float64 value in a Fyne application
type Float interface {
DataItem
Get() float64
Set(float64)
}

// NewFloat returns a bindable float value that is managed internally.
func NewFloat() Float {
blank := 0.0
return &floatBind{val: &blank}
}

// BindFloat returns a new bindable value that controls the contents of the provided float64 variable.
func BindFloat(f *float64) Float {
if f == nil {
return NewFloat() // never allow a nil value pointer
}

return &floatBind{val: f}
}

type floatBind struct {
base

val *float64
}

func (f *floatBind) Get() float64 {
if f.val == nil {
return 0.0
}
return *f.val
}

func (f *floatBind) Set(val float64) {
*f.val = val

f.trigger(f)
}
24 changes: 24 additions & 0 deletions binding/float_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package binding

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestBindFloat(t *testing.T) {
val := 0.5
f := BindFloat(&val)
assert.Equal(t, float64(0.5), f.Get())

f.Set(0.3)
assert.Equal(t, float64(0.3), val)
}

func TestNewFloat(t *testing.T) {
f := NewFloat()
assert.Equal(t, float64(0.0), f.Get())

f.Set(0.3)
assert.Equal(t, float64(0.3), f.Get())
}
42 changes: 42 additions & 0 deletions cmd/fyne_demo/tutorials/bind.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package tutorials

import (
"fmt"

"fyne.io/fyne"
"fyne.io/fyne/binding"
"fyne.io/fyne/container"
"fyne.io/fyne/widget"
)

func bindingScreen(_ fyne.Window) fyne.CanvasObject {
data := binding.NewFloat()
label := widget.NewLabel(floatLabel(data))
data.AddListener(binding.NewNotifyFunction(func(binding.DataItem) {
label.SetText(floatLabel(data))
}))

slide := widget.NewSliderWithData(0, 1, data)
slide.Step = 0.1
bar := widget.NewProgressBarWithData(data)

buttons := container.NewGridWithColumns(4,
widget.NewButton("0%", func() {
data.Set(0)
}),
widget.NewButton("30%", func() {
data.Set(0.3)
}),
widget.NewButton("70%", func() {
data.Set(0.7)
}),
widget.NewButton("100%", func() {
data.Set(1)
}))

return container.NewVBox(label, slide, bar, buttons)
}

func floatLabel(data binding.Float) string { // TODO add type conversion
return fmt.Sprintf("Float current value: %0.1f", data.Get())
}
5 changes: 4 additions & 1 deletion cmd/fyne_demo/tutorials/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ var (
"Window function demo.",
windowScreen,
},
"binding": {"Data Binding",
"Connecting widgets to a data source.",
bindingScreen},
"advanced": {"Advanced",
"Debug and advanced information.",
advancedScreen,
Expand All @@ -131,7 +134,7 @@ var (

// TutorialIndex defines how our tutorials should be laid out in the index tree
TutorialIndex = map[string][]string{
"": {"welcome", "canvas", "icons", "widgets", "collections", "containers", "dialogs", "windows", "advanced"},
"": {"welcome", "canvas", "icons", "widgets", "collections", "containers", "dialogs", "windows", "binding", "advanced"},
"collections": {"list", "table", "tree"},
"containers": {"apptabs", "border", "box", "center", "grid", "split", "scroll"},
"widgets": {"accordion", "button", "card", "entry", "form", "input", "text", "toolbar", "progress"},
Expand Down
14 changes: 14 additions & 0 deletions widget/progressbar.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"image/color"

"fyne.io/fyne"
"fyne.io/fyne/binding"
"fyne.io/fyne/canvas"
"fyne.io/fyne/internal/widget"
"fyne.io/fyne/theme"
Expand Down Expand Up @@ -123,3 +124,16 @@ func NewProgressBar() *ProgressBar {
Renderer(p).Layout(p.MinSize())
return p
}

// NewProgressBarWithData returns a progress bar connected with the specified data source.
func NewProgressBarWithData(data binding.Float) *ProgressBar {
p := NewProgressBar()
p.Value = data.Get()

data.AddListener(binding.NewNotifyFunction(func(d binding.DataItem) {
p.Value = d.(binding.Float).Get()
p.Refresh()
}))

return p
}
17 changes: 17 additions & 0 deletions widget/slider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"math"

"fyne.io/fyne"
"fyne.io/fyne/binding"
"fyne.io/fyne/canvas"
"fyne.io/fyne/internal/widget"
"fyne.io/fyne/theme"
Expand Down Expand Up @@ -46,6 +47,22 @@ func NewSlider(min, max float64) *Slider {
return slider
}

// NewSliderWithData returns a slider connected with the specified data source.
func NewSliderWithData(min, max float64, data binding.Float) *Slider {
slider := NewSlider(min, max)
slider.Value = data.Get()

data.AddListener(binding.NewNotifyFunction(func(binding.DataItem) {
slider.Value = data.Get()
slider.Refresh()
}))
slider.OnChanged = func(f float64) {
data.(binding.Float).Set(f)
}

return slider
}

// DragEnd function.
func (s *Slider) DragEnd() {
}
Expand Down

0 comments on commit 7154e4c

Please sign in to comment.