Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spinner Widget #101

Open
wants to merge 73 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
c6c0394
Begin definition of a Spinner widget.
jimorc Jan 12, 2025
f6c4c56
Add barebones spinnerButton widget
jimorc Jan 13, 2025
01c34d2
Add a spinnerRenderer
jimorc Jan 13, 2025
c1dfd6d
Position and size Spinner box and text.
jimorc Jan 13, 2025
f503356
Display the upButton object in the Spinner widget.
jimorc Jan 13, 2025
9eb163d
Add downButton
jimorc Jan 13, 2025
ba6a932
Remove unneeded spinnerButton.Refresh calls.
jimorc Jan 13, 2025
c543ded
Remove unneeded spinnerButton.Refresh calls.
jimorc Jan 13, 2025
7209fe6
Add/subtract step from value when buttons pressed
jimorc Jan 13, 2025
65a5880
Fix width of Spinner
jimorc Jan 14, 2025
211fe9c
Move background to spinnerButtonRenderer
jimorc Jan 14, 2025
ccb959c
Add arrows to spinner buttons
jimorc Jan 14, 2025
6d16b0e
Change Spinner backgound color
jimorc Jan 14, 2025
e5e39c1
Change spinner backgound to transparent
jimorc Jan 14, 2025
e4187bd
Disable upButton or downButton if value == max or min.
jimorc Jan 14, 2025
5db832f
Add Spinner.SetValue method
jimorc Jan 14, 2025
61b050d
Change Spinner background when mouse hovers over widget
jimorc Jan 14, 2025
c13042f
Change border color when Spinner gains/loses focus.
jimorc Jan 14, 2025
8b8f5fb
Request focus on mouse down
jimorc Jan 15, 2025
a013170
Handle processing of disabled Spinner.
jimorc Jan 15, 2025
4bfcb37
Add Spinner.GetValue method
jimorc Jan 15, 2025
fc1defb
Add tests for NewSpinner and Spinner.SetValue
jimorc Jan 15, 2025
c5275ea
Enable/disable buttons when Spinner enabled/disabled
jimorc Jan 15, 2025
25977f0
Add enable/disable tests
jimorc Jan 15, 2025
37b6d10
Increment/decrement value based on keyboard input
jimorc Jan 15, 2025
f1a9e89
Show button hovered over.
jimorc Jan 16, 2025
20ecea3
REmove debug print
jimorc Jan 16, 2025
0193834
Handle mouse scroll
jimorc Jan 16, 2025
09f8ae8
Handle mouse scroller input
jimorc Jan 16, 2025
da93571
Add Spinner.OnChanged
jimorc Jan 16, 2025
f6ca207
Fix bug in main
jimorc Jan 16, 2025
750a1c9
Fix bug in main
jimorc Jan 16, 2025
f8ad780
Add binding to Spinner widget
jimorc Jan 18, 2025
8c36f7e
Add test for setting value outside range
jimorc Jan 18, 2025
e0541ae
Update Spinner example
jimorc Jan 18, 2025
6680150
Update demo program
jimorc Jan 18, 2025
035437e
Update README.md
jimorc Jan 19, 2025
e440f99
Fix bug
jimorc Jan 19, 2025
7822759
Fix bug on placement of text and buttons
jimorc Jan 19, 2025
c07fabf
Place second label and spinner in an HBox to show min spinner size.
jimorc Jan 19, 2025
52953ca
Constrain spinner min, max, step to logical values
jimorc Jan 19, 2025
2c38220
Test Unbind()
jimorc Jan 19, 2025
1c4a9e2
Update README.md
jimorc Jan 19, 2025
1c29186
Update README.md
jimorc Jan 19, 2025
02c5e91
Add Spinner.SetMinMaxStep method
jimorc Jan 19, 2025
a57614e
Adjust Spinner value if outside min:max range
jimorc Jan 20, 2025
b882e96
Allow creation of unitialized Spinner
jimorc Jan 21, 2025
4eb22a9
Replace custom spinnerButton with extended widget.Button
jimorc Jan 23, 2025
afff3c6
Change button icons
jimorc Jan 24, 2025
cc4d52e
Change Spinner to IntSpinner
jimorc Jan 25, 2025
c5a5114
Create a baseSpinnerButton
jimorc Jan 25, 2025
bc41658
Add Float64Spinner widget
jimorc Jan 25, 2025
16c9cc7
Move MouseIn, MouseMove, MouseOut to baseSpinner
jimorc Jan 26, 2025
98c37e9
Extract out common canvas.Text size functionality
jimorc Jan 26, 2025
ebd6d76
Move spinnerButton MinSize to baseSpinnerButton
jimorc Jan 26, 2025
ad47db4
Move size calculation to setButtonProperties
jimorc Jan 26, 2025
3768bf4
Add newSpinnerButton function
jimorc Jan 26, 2025
9d17b6f
Rename baseSpinnerButton to spinnerButton
jimorc Jan 26, 2025
675170c
Delete spinnerButton.Resize
jimorc Jan 26, 2025
efd8b87
Remove comment out code.
jimorc Jan 26, 2025
a1ae925
Move upButton, downButton to baseSpinner
jimorc Jan 26, 2025
b474383
Refactor IntSpinner.spinnerColors and Float64Spinner.spinnerColors in…
jimorc Jan 26, 2025
2cc41e3
Remove unnecessary calls to canvas.Text.Refresh
jimorc Jan 26, 2025
31b03f2
Move text alignment setting to CreateRenderer
jimorc Jan 26, 2025
a469aea
Add spinnerButton.enableDisable method
jimorc Jan 26, 2025
9c77bff
Update main to display 2 digits in spinner
jimorc Jan 26, 2025
f5abb75
Update README to present state of spinner widgets.
jimorc Jan 26, 2025
d39fddc
Replace IntSpinner and Float64Spinner with single Spinner widget
jimorc Jan 28, 2025
f432a45
Remove baseSpinner
jimorc Jan 28, 2025
59c7abb
Changed allowed format from %-d to %+d.
jimorc Jan 28, 2025
8b32c23
Modify some display formats
jimorc Jan 28, 2025
fc5b74b
Correct the format comment.
jimorc Jan 28, 2025
b28d666
Update README.md to match the new Spinner.
jimorc Jan 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,76 @@ m := NewMap()

![](img/map.png)

### Spinner

A Spinner is a widget that displays a numerical value along with an increment (up) button and a decrement (down)
button. These buttons
allow incrementing or decrementing the value.
The spinner's value is limited to be between the minimum and
maximum values, and the value increments or decrements by the step value. The initially displayed
value is set to the minimum value, or to the bound value.

All Spinner values (value, minumum, maximum, and step) are stored as float64 values. There is a format
field that determines how the value is displayed in the Spinner widget. If the format contains "%d" or
"%+d", the value is displayed as an integer. Other "%" formats assume you want to display the value as
a floating point number. It is recommended that you use "%.Xf", where X is an unsigned integer, as the
format for displaying floating point numbers.

You may include other text in the format. For example, if the current value is 47.15, and you specify the
format as "%d %%" then the displayed value will be "47 %"

The most typical step value for an integer Spinner
value would be 1, but the step value can be set to any value greater than 0 and less than or equal to the
maximum value minus the minimum value.

In addition to clicking on the up and down buttons, the spinner value can be incremented or decremented using either
keyboard keys, or the mouse scroller or touchpad. Clicking on one of the buttons
will modify the spinner value at any time, except when the spinner is disabled. Keyboard, scroller, and touchpad
input only works when the spinner has focus.

Here are the keyboard keys accepted by the spinner widget and their effects:

| Key | Effect |
|---|---|
| KeyUp | Increment value by step amount |
| + | Increment value by step amount |
| KeyDown | Decrement value by step amount |
| - | Decrement value by step amount |

Finally, the value can be set programmatically using the `Spinner.SetValue` method.

Here is simple example that creates a spinner widget:

```go
spinner := NewSpinner(2, 22, 3.14, "%.1f", valChanged)
spinner.SetValue(6)
```

The result of executing this code is an IntSpinner widget with the following settings:

| Setting | Value |
|---|---|
| minimum value | 2.0 |
| maximum value | 22.0 |
| step | 3.14 |
| format | "%.1f" |
|initial value | 2.0 |
| function called on value change | valChanged |
| value after SetValue call | 6.0 |

This is what a Spinner, with the format of "%d" and that does not have focus, looks like in light theme:

![](img/spinner-light.png)

and this is a focused Spinner with the format of "%.1f" in dark theme:

![](img/spinner-dark.png)

The spinner is normally sized to the minimum width and height required to display any value between its
minimum and maximum values, although the width and height may be controlled by the container it is included in.

See the [demo](cmd/spinner_demo/main.go) program for an example of how to use the spinner widget.

### TwoStateToolbarAction

A TwoStateToolbarAction displays one of two icons based on the stored state. It is similar
Expand Down
98 changes: 98 additions & 0 deletions cmd/spinner_demo/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package main

import (
"fmt"
"strconv"

"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/widget"
xwidget "fyne.io/x/fyne/widget"
)

var spinnerDisabled bool
var data binding.Float = binding.NewFloat()
var s1 *xwidget.Spinner
var s5 *xwidget.Spinner
var bs *widget.Button

func main() {
a := app.New()

ls1 := widget.NewLabel("Value set in Spinner 1:")
s1ValueLabel := widget.NewLabel("")
ls2 := widget.NewLabel("Data value bound to Spinner 2:")
dataValueLabel := widget.NewLabel("")
data.AddListener(binding.NewDataListener(func() {
val, err := data.Get()
if err != nil {
return
}
dataValueLabel.Text = fmt.Sprintf("%d", int(val))
dataValueLabel.Refresh()
}))

ls5 := widget.NewLabel("Value set in Spinner 5:")
s5ValueLabel := widget.NewLabel("")
floatData := binding.NewFloat()
floatData.AddListener(binding.NewDataListener(func() {
val, err := floatData.Get()
if err != nil {
return
}
s5ValueLabel.Text = strconv.FormatFloat(val, 'f', 3, 64)
s5ValueLabel.Refresh()
}))
c6 := container.NewHBox(ls5, s5ValueLabel)

c2 := container.NewGridWithColumns(2, ls1, s1ValueLabel, ls2, dataValueLabel)
l1 := widget.NewLabel("Spinner 1 (0, 100, 1, \"%d %%\"):")
s1 = xwidget.NewSpinner(0, 100, 1, "%d %%", nil)
s1.OnChanged = func(val float64) {
s1ValueLabel.Text = fmt.Sprintf("%d %%", int(s1.GetValue()))
s1ValueLabel.Refresh()
}
// OnChanged has to be called here to display initial value in s1ValueLabel.
s1.OnChanged(s1.GetValue())
l2 := widget.NewLabel("Spinner 2 With Data (-2, 16, 3, \"%+d\"):")
s2 := xwidget.NewSpinnerWithData(-2, 16, 3, "%+d", data)
c := container.NewGridWithColumns(2, l1, s1)
c1 := container.NewHBox(l2, s2)
l3 := widget.NewLabel("Uninitialized Spinner 3:")
s3 := xwidget.NewSpinnerUninitialized("%d")
c3 := container.NewHBox(l3, s3)
b := widget.NewButton("Disable Spinner 1", func() {})
b.OnTapped = func() {
spinnerDisabled = !spinnerDisabled
if spinnerDisabled {
s1.Disable()
b.SetText("Enable Spinner 1")
} else {
s1.Enable()
b.SetText("Disable Spinner 1")
}
}
bs = widget.NewButton("Initialize Spinner 3", func() {
s3.SetMinMaxStep(1, 10, 1)
l3.Text = "Initialized Spinner 3 (1, 10, 1, \"%d\"):"
l3.Refresh()
s3.Enable()
bs.Disable()
})
bs1 := widget.NewButton("Set Spinner 1 to 5", func() { s1.SetValue(5) })
bs2 := widget.NewButton("Set Spinner 3 bound value to 12", func() { data.Set(12) })

l4 := widget.NewLabel("Spinner 4 (-1., 400., 10.3, \"%+.1f\"):")
s4 := xwidget.NewSpinner(-1., 400., 10.3, "%.1f", nil)
c4 := container.NewHBox(l4, s4)

l5 := widget.NewLabel("Spinner 5 (0., 16., 3.215, \"%.2f\")")
s5 = xwidget.NewSpinnerWithData(0., 16., 3.215, "%.2f", floatData)
c5 := container.NewHBox(l5, s5)

v := container.NewVBox(c, c1, c3, b, bs, c2, bs1, bs2, c4, c5, c6)
w := a.NewWindow("SpinnerDemo")
w.SetContent(v)
w.ShowAndRun()
}
Binary file added img/spinner-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/spinner-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 79 additions & 0 deletions widget/binder_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package widget

// This file is a direct copy of widget/binder_helper.go in fyne version 2.5.3.

import (
"sync"
"sync/atomic"

"fyne.io/fyne/v2/data/binding"
)

// basicBinder stores a DataItem and a function to be called when it changes.
// It provides a convenient way to replace data and callback independently.
type basicBinder struct {
callback atomic.Pointer[func(binding.DataItem)]

dataListenerPairLock sync.RWMutex
dataListenerPair annotatedListener // access guarded by dataListenerPairLock
}

// Bind replaces the data item whose changes are tracked by the callback function.
func (binder *basicBinder) Bind(data binding.DataItem) {
listener := binding.NewDataListener(func() { // NB: listener captures `data` but always calls the up-to-date callback
f := binder.callback.Load()
if f == nil || *f == nil {
return
}

(*f)(data)
})
data.AddListener(listener)
listenerInfo := annotatedListener{
data: data,
listener: listener,
}

binder.dataListenerPairLock.Lock()
binder.unbindLocked()
binder.dataListenerPair = listenerInfo
binder.dataListenerPairLock.Unlock()
}

// CallWithData passes the currently bound data item as an argument to the
// provided function.
func (binder *basicBinder) CallWithData(f func(data binding.DataItem)) {
binder.dataListenerPairLock.RLock()
data := binder.dataListenerPair.data
binder.dataListenerPairLock.RUnlock()
f(data)
}

// SetCallback replaces the function to be called when the data changes.
func (binder *basicBinder) SetCallback(f func(data binding.DataItem)) {
binder.callback.Store(&f)
}

// Unbind requests the callback to be no longer called when the previously bound
// data item changes.
func (binder *basicBinder) Unbind() {
binder.dataListenerPairLock.Lock()
binder.unbindLocked()
binder.dataListenerPairLock.Unlock()
}

// unbindLocked expects the caller to hold dataListenerPairLock.
func (binder *basicBinder) unbindLocked() {
previousListener := binder.dataListenerPair
binder.dataListenerPair = annotatedListener{nil, nil}

if previousListener.listener == nil || previousListener.data == nil {
return
}
previousListener.data.RemoveListener(previousListener.listener)
}

type annotatedListener struct {
data binding.DataItem
listener binding.DataListener
}
Loading