Skip to content

Commit

Permalink
ui: Add support for ListBox component (#2)
Browse files Browse the repository at this point in the history
* Inital listbox support for FLTK

* Listbox -> ListBox

* Add to "Hello Spot!" example

* Add Cocoa implementation \o/

* Disable some logs
  • Loading branch information
roblillack authored May 24, 2024
1 parent ad1129d commit 846ebba
Show file tree
Hide file tree
Showing 7 changed files with 309 additions and 18 deletions.
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,22 +185,22 @@ Explanation of the status column: \
⚠️ Partially implemented /
✅ Done

| Name | Description | Native controls used | Status |
| --------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| [Button](https://pkg.go.dev/github.com/roblillack/spot/ui#Button) | Simple button to initiate an action | [Fl_Button](https://www.fltk.org/doc-1.4/classFl__Button.html) <br> NSButton ||
| [Checkbox](https://pkg.go.dev/github.com/roblillack/spot/ui#Checkbox) | Control offering the user a choice between two mutually exclusive options | [Fl_Check_Button](https://www.fltk.org/doc-1.4/classFl__Check__Button.html) <br> NSButton ||
| ComboBox | A combined dropdown menu with text input | ComboBox <br> NSComboBox | Not started |
| [Dial](https://pkg.go.dev/github.com/roblillack/spot/ui#Dial) | Circular status control | [Fl_Dial](https://www.fltk.org/doc-1.4/classFl__Dial.html) <br> NSProgressIndicator (with `NSCircular` style) | ⚠️ |
| [Dropdown](https://pkg.go.dev/github.com/roblillack/spot/ui#Dropdown) | Drop-down menu to select a single item out of multiple options | [Fl_Choice](https://www.fltk.org/doc-1.4/classFl__Choice.html) <br> NSPopUpButton ||
| Image | An image control | Image <br> NSImageView | Not started |
| [Label](https://pkg.go.dev/github.com/roblillack/spot/ui#Label) | Simple, non-editable text label | [Fl_Box](https://www.fltk.org/doc-1.4/classFl__Box.html) <br> [NSTextField](https://developer.apple.com/documentation/appkit/nstextfield) ||
| ListBox | A list control | List <br> NSTableView | 🚧 |
| [ProgressBar](https://pkg.go.dev/github.com/roblillack/spot/ui#ProgressBar) | Progress bar control to visualize the progression of a long-running operation | Progress <br> NSProgress ||
| [Slider](https://pkg.go.dev/github.com/roblillack/spot/ui#Slider) | Horizontal slider input control | [Fl_Slider](https://www.fltk.org/doc-1.4/classFl__Slider.html) <br> NSSlider ||
| [Spinner](https://pkg.go.dev/github.com/roblillack/spot/ui#Spinner) | Number input control with up/down buttons | [Fl_Spinner](https://www.fltk.org/doc-1.4/classFl__Spinner.html) <br> [NSTextField](https://developer.apple.com/documentation/appkit/nstextfield)+[NSStepper](https://developer.apple.com/documentation/appkit/nsstepper) ||
| [TextField](https://pkg.go.dev/github.com/roblillack/spot/ui#TextField) | Control for single-line text input | [Fl_Input](https://www.fltk.org/doc-1.4/classFl__Input.html) <br> [NSTextField](https://developer.apple.com/documentation/appkit/nstextfield) ||
| TextView/TextEditor | General-purpose text box to view/edit multi-line text content | Text <br> NSTextView | 🚧 |
| [Window](https://pkg.go.dev/github.com/roblillack/spot/ui#Window) | Control representing a (top-level) window on the screen | [Fl_Window](https://www.fltk.org/doc-1.4/classFl__Window.html) <br> NSWindow ||
| Name | Description | Native controls used | Status |
| --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| [Button](https://pkg.go.dev/github.com/roblillack/spot/ui#Button) | Simple button to initiate an action | [Fl_Button](https://www.fltk.org/doc-1.4/classFl__Button.html) <br> NSButton ||
| [Checkbox](https://pkg.go.dev/github.com/roblillack/spot/ui#Checkbox) | Control offering the user a choice between two mutually exclusive options | [Fl_Check_Button](https://www.fltk.org/doc-1.4/classFl__Check__Button.html) <br> NSButton ||
| ComboBox | A combined dropdown menu with text input | ComboBox <br> NSComboBox | Not started |
| [Dial](https://pkg.go.dev/github.com/roblillack/spot/ui#Dial) | Circular status control | [Fl_Dial](https://www.fltk.org/doc-1.4/classFl__Dial.html) <br> NSProgressIndicator (with `NSCircular` style) | ⚠️ |
| [Dropdown](https://pkg.go.dev/github.com/roblillack/spot/ui#Dropdown) | Drop-down menu to select a single item out of multiple options | [Fl_Choice](https://www.fltk.org/doc-1.4/classFl__Choice.html) <br> NSPopUpButton ||
| Image | An image control | Image <br> NSImageView | Not started |
| [Label](https://pkg.go.dev/github.com/roblillack/spot/ui#Label) | Simple, non-editable text label | [Fl_Box](https://www.fltk.org/doc-1.4/classFl__Box.html) <br> [NSTextField](https://developer.apple.com/documentation/appkit/nstextfield) ||
| [ListBox](https://pkg.go.dev/github.com/roblillack/spot/ui#ListBox) | Scrollable control which allows the user to select a single or multible items from a given list | [Fl_Select_Browser](https://www.fltk.org/doc-1.4/classFl__Select__Browser.html)/[Fl_Multi_Browser](https://www.fltk.org/doc-1.4/classFl__Multi__Browser.html)<br> [NSTableView](https://developer.apple.com/documentation/appkit/nstableview) | |
| [ProgressBar](https://pkg.go.dev/github.com/roblillack/spot/ui#ProgressBar) | Progress bar control to visualize the progression of a long-running operation | Progress <br> NSProgress ||
| [Slider](https://pkg.go.dev/github.com/roblillack/spot/ui#Slider) | Horizontal slider input control | [Fl_Slider](https://www.fltk.org/doc-1.4/classFl__Slider.html) <br> NSSlider ||
| [Spinner](https://pkg.go.dev/github.com/roblillack/spot/ui#Spinner) | Number input control with up/down buttons | [Fl_Spinner](https://www.fltk.org/doc-1.4/classFl__Spinner.html) <br> [NSTextField](https://developer.apple.com/documentation/appkit/nstextfield)+[NSStepper](https://developer.apple.com/documentation/appkit/nsstepper) ||
| [TextField](https://pkg.go.dev/github.com/roblillack/spot/ui#TextField) | Control for single-line text input | [Fl_Input](https://www.fltk.org/doc-1.4/classFl__Input.html) <br> [NSTextField](https://developer.apple.com/documentation/appkit/nstextfield) ||
| TextView/TextEditor | General-purpose text box to view/edit multi-line text content | Text <br> NSTextView | 🚧 |
| [Window](https://pkg.go.dev/github.com/roblillack/spot/ui#Window) | Control representing a (top-level) window on the screen | [Fl_Window](https://www.fltk.org/doc-1.4/classFl__Window.html) <br> NSWindow ||

## Potential future backends to look at

Expand Down
11 changes: 10 additions & 1 deletion examples/hello-spot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,6 @@ func main() {
OnValueChanged: func(value float64) {
setCounter(int(value))
},
// Type: gocoa.SliderTypeLinear,
},
&ui.Button{
X: 210,
Expand All @@ -209,6 +208,16 @@ func main() {
},
&ui.Label{X: 210, Y: 100, Width: 180, Height: 25, Value: "Current backend:"},
&BlinkingLabel{X: 210, Y: 120, Width: 180, Height: 30, Text: ui.BackendName, Size: 20},
&ui.ListBox{
X: 210, Y: 285, Width: 180, Height: 75,
Values: []string{"Null", "Eins", "Zwei", "Drei", "Vier", "Fünf", "Sechs", "Sieben", "Acht", "Neun"},
Selection: []int{counter % 10},
OnSelect: func(selection []int) {
if len(selection) > 0 {
setCounter(selection[0])
}
},
},
spot.Make(QuitButton),
&ui.TextView{
X: 10, Y: 10, Width: 380, Height: 80,
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.18

require (
github.com/pwiecz/go-fltk v0.0.0-20240511142305-990b442ae1ed
github.com/roblillack/gocoa v0.2.0
github.com/roblillack/gocoa v0.3.0
)

// replace github.com/roblillack/gocoa => ./libs/gocoa
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ github.com/roblillack/gocoa v0.1.0 h1:JnwpvHBKo0tbu0mEawF7SKpwu/CV4iNneq70FegDwB
github.com/roblillack/gocoa v0.1.0/go.mod h1:zySUFzLK/KSb3KDT903dTRJ2WPmMBbF3a2u8/39UzMg=
github.com/roblillack/gocoa v0.2.0 h1:7NAjTPyKGd9mcqvZSish1b5qSU3zx6ISBHrTU4Ieph4=
github.com/roblillack/gocoa v0.2.0/go.mod h1:zySUFzLK/KSb3KDT903dTRJ2WPmMBbF3a2u8/39UzMg=
github.com/roblillack/gocoa v0.3.0 h1:ZvEulTbrJHgmeweJJrHZhoGvUzgD/Q54Pmc+cJ5sJyo=
github.com/roblillack/gocoa v0.3.0/go.mod h1:zySUFzLK/KSb3KDT903dTRJ2WPmMBbF3a2u8/39UzMg=
24 changes: 24 additions & 0 deletions ui/listbox.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package ui

import (
"github.com/roblillack/spot"
)

type ListBox struct {
X int
Y int
Width int
Height int
Values []string
Multiselect bool
Selection []int
OnSelect func([]int)
ref nativeTypeListBox
}

var _ spot.Component = &ListBox{}
var _ spot.Control = &ListBox{}

func (c *ListBox) Render(ctx *spot.RenderContext) spot.Component {
return c
}
113 changes: 113 additions & 0 deletions ui/listbox_cocoa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//go:build !fltk && (darwin || cocoa)

package ui

import (
"slices"

"github.com/roblillack/gocoa"
"github.com/roblillack/spot"
)

type nativeTypeListBox = *gocoa.TableView

func (c *ListBox) getSelection() []int {
if c.ref == nil {
return nil
}

if !c.Multiselect {
for i := 0; i < c.ref.NumberOfRows(); i++ {
if c.ref.IsRowSelected(i) {
return []int{i}
}
}

return []int{}
}

var selection []int
for i := 0; i < c.ref.NumberOfRows(); i++ {
if c.ref.IsRowSelected(i) {
selection = append(selection, i)
}
}

return selection
}

func (c *ListBox) setValues(values []string) {
c.Values = values

if c.ref == nil {
return
}

c.ref.Clear()
for _, v := range values {
c.ref.Add(v)
}
}

func (c *ListBox) setSelection(selection []int) {
c.Selection = selection

if c.ref == nil {
return
}

c.ref.DeselectAll()

for _, i := range selection {
c.ref.SelectRowIndex(i)
c.ref.ScrollRowToVisible(i)
}
}

func (c *ListBox) Update(nextComponent spot.Control) bool {
next, ok := nextComponent.(*ListBox)
if !ok {
return false
}

if c.ref == nil {
return false
}

if !slices.Equal(c.Values, next.Values) {
c.setValues(next.Values)
}

if !slices.Equal(c.Selection, next.Selection) {
c.setSelection(next.Selection)
}

c.OnSelect = next.OnSelect
c.ref.OnSelectionDidChange(c.OnSelect)

if c.Multiselect != next.Multiselect {
c.Multiselect = next.Multiselect
c.ref.SetAllowsMultipleSelection(c.Multiselect)
}

return true
}

func (c *ListBox) Mount(parent spot.Control) any {
if c.ref != nil {
return c.ref
}

c.ref = gocoa.NewTableView(c.X, c.Y, c.Width, c.Height)
c.ref.SetAllowsMultipleSelection(c.Multiselect)

c.setValues(c.Values)
c.setSelection(c.Selection)
c.ref.OnSelectionDidChange(c.OnSelect)

if window, ok := parent.(*Window); ok && window != nil && window.ref != nil {
window.ref.AddTableView(c.ref)
}

return c.ref
}
143 changes: 143 additions & 0 deletions ui/listbox_fltk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
//go:build !cocoa && (fltk || !darwin)

package ui

import (
"slices"

goFltk "github.com/pwiecz/go-fltk"
"github.com/roblillack/spot"
)

type nativeTypeListBox = *goFltk.Browser

func (c *ListBox) getSelection() []int {
if c.ref == nil {
return nil
}

if !c.Multiselect {
if c.ref.Value() == 0 {
return nil
}

return []int{c.ref.Value() - 1}
}

var selection []int
for i := 0; i < c.ref.Size(); i++ {
if c.ref.IsSelected(i + 1) {
selection = append(selection, i)
}
}

return selection
}

func (c *ListBox) setValues(values []string) {
c.Values = values

if c.ref == nil {
return
}

c.ref.Clear()
for _, v := range values {
c.ref.Add(v)
}
}

func (c *ListBox) setSelection(selection []int) {
c.Selection = selection

if c.ref == nil {
return
}

for i := 0; i < c.ref.Size(); i++ {
c.ref.SetSelected(i+1, false)
}

for _, i := range selection {
c.ref.SetSelected(i+1, true)
}
}

func (c *ListBox) callback() {
if c.ref == nil {
return
}

oldSelection := c.Selection

c.Selection = c.getSelection()
// fmt.Printf("LISTBOX[%p] callback, multi: %v, selection: %v\n", c, c.Multiselect, c.Selection)

if !c.Multiselect {
c.setSelection(c.Selection)
}

if c.OnSelect != nil && !slices.Equal(oldSelection, c.Selection) {
c.OnSelect(c.Selection)
}
}

func (c *ListBox) Update(nextComponent spot.Control) bool {
// fmt.Printf("LISTBOX[%p] Update\n", c)
next, ok := nextComponent.(*ListBox)
if !ok {
return false
}

if c.ref == nil {
return false
}

if !slices.Equal(c.Values, next.Values) {
c.setValues(next.Values)
}

if !slices.Equal(c.Selection, next.Selection) {
c.setSelection(next.Selection)
}

c.OnSelect = next.OnSelect

if c.Multiselect != next.Multiselect {
c.Multiselect = next.Multiselect
if c.ref != nil {
parent := c.ref.Parent()
c.ref.Destroy()
c.ref = nil
c.Mount(nil)
parent.Add(c.ref)
}
}

return true
}

func (c *ListBox) Mount(parent spot.Control) any {
if c.ref != nil {
return c.ref
}

if c.Multiselect {
ref := goFltk.NewMultiBrowser(c.X, c.Y, c.Width, c.Height)
browser := ref.Browser
c.ref = &browser
} else {
ref := goFltk.NewSelectBrowser(c.X, c.Y, c.Width, c.Height)
browser := ref.Browser
c.ref = &browser
}
c.ref.SetCallback(c.callback)
c.setValues(c.Values)
c.setSelection(c.Selection)

if window, ok := parent.(*Window); ok && window != nil && window.ref != nil {
window.ref.Add(c.ref)
}

return c.ref
}

0 comments on commit 846ebba

Please sign in to comment.