Add SelectBox Widget
This commit is contained in:
parent
8f91971b62
commit
6df7bade48
|
@ -8,14 +8,14 @@ applications (SDL2, for Linux, MacOS and Windows) as well as web browsers
|
||||||
|
|
||||||
![Screenshot](docs/guitest.png)
|
![Screenshot](docs/guitest.png)
|
||||||
|
|
||||||
> _(Screenshot is from Project: Doodle's GUITest debug screen showing a_
|
> _(Screenshot is from Sketchy Maze's GUITest debug screen showing a_
|
||||||
> _Window, several Frames, Labels, Buttons and a Checkbox widget.)_
|
> _Window, several Frames, Labels, Buttons and a Checkbox widget.)_
|
||||||
|
|
||||||
It is very much a **work in progress** and may contain bugs and its API may
|
It is very much a **work in progress** and may contain bugs and its API may
|
||||||
change as bugs are fixed or features added.
|
change as bugs are fixed or features added.
|
||||||
|
|
||||||
This library is being developed in conjunction with my drawing-based maze
|
This library is being developed in conjunction with my drawing-based maze
|
||||||
game, [Project: Doodle](https://www.kirsle.net/doodle). The rendering engine
|
game, [Sketchy Maze](https://www.sketchymaze.com). The rendering engine
|
||||||
library is at [go/render](https://git.kirsle.net/go/render) which provides
|
library is at [go/render](https://git.kirsle.net/go/render) which provides
|
||||||
the SDL2 and Canvas back-ends.
|
the SDL2 and Canvas back-ends.
|
||||||
(GitHub mirror: [kirsle/render](https://github.com/kirsle/render))
|
(GitHub mirror: [kirsle/render](https://github.com/kirsle/render))
|
||||||
|
@ -148,13 +148,13 @@ most complex.
|
||||||
provides a simple API to add menus and items to it.
|
provides a simple API to add menus and items to it.
|
||||||
* [x] **Menu**: a frame full of clickable links and separators. Usually used as
|
* [x] **Menu**: a frame full of clickable links and separators. Usually used as
|
||||||
a modal pop-up by the MenuButton and MenuBar.
|
a modal pop-up by the MenuButton and MenuBar.
|
||||||
|
* [x] **SelectBox**: a kind of MenuButton that lets the user choose a
|
||||||
|
value from a list of possible values.
|
||||||
|
|
||||||
**Work in progress widgets:**
|
**Work in progress widgets:**
|
||||||
|
|
||||||
* [ ] **Scrollbar**: a Frame including a trough, scroll buttons and a
|
* [ ] **Scrollbar**: a Frame including a trough, scroll buttons and a
|
||||||
draggable slider.
|
draggable slider.
|
||||||
* [ ] **SelectBox:** a kind of MenuButton that lets the user choose a value
|
|
||||||
from a list of possible values, bound to a string variable.
|
|
||||||
|
|
||||||
**Wish list for the longer-term future:**
|
**Wish list for the longer-term future:**
|
||||||
|
|
||||||
|
|
114
eg/forms/main.go
Normal file
114
eg/forms/main.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/render/sdl"
|
||||||
|
"git.kirsle.net/go/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
sdl.DefaultFontFilename = "../DejaVuSans.ttf"
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
mw, err := ui.NewMainWindow("Forms Test")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mw.SetBackground(render.White)
|
||||||
|
|
||||||
|
// Buttons row.
|
||||||
|
{
|
||||||
|
frame := ui.NewFrame("Frame 1")
|
||||||
|
mw.Pack(frame, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
FillX: true,
|
||||||
|
Padding: 4,
|
||||||
|
})
|
||||||
|
label := ui.NewLabel(ui.Label{
|
||||||
|
Text: "Buttons:",
|
||||||
|
})
|
||||||
|
frame.Pack(label, ui.Pack{
|
||||||
|
Side: ui.W,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Buttons.
|
||||||
|
btn := ui.NewButton("Button 1", ui.NewLabel(ui.Label{
|
||||||
|
Text: "Click me!",
|
||||||
|
}))
|
||||||
|
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
||||||
|
fmt.Println("Clicked!")
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
frame.Pack(btn, ui.Pack{
|
||||||
|
Side: ui.W,
|
||||||
|
PadX: 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
mw.Supervisor().Add(btn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selectbox row.
|
||||||
|
{
|
||||||
|
frame := ui.NewFrame("Frame 2")
|
||||||
|
mw.Pack(frame, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
FillX: true,
|
||||||
|
Padding: 4,
|
||||||
|
})
|
||||||
|
label := ui.NewLabel(ui.Label{
|
||||||
|
Text: "Set window color:",
|
||||||
|
})
|
||||||
|
frame.Pack(label, ui.Pack{
|
||||||
|
Side: ui.W,
|
||||||
|
})
|
||||||
|
|
||||||
|
var colors = []struct{
|
||||||
|
Label string
|
||||||
|
Value render.Color
|
||||||
|
}{
|
||||||
|
{"White", render.White},
|
||||||
|
{"Yellow", render.Yellow},
|
||||||
|
{"Cyan", render.Cyan},
|
||||||
|
{"Green", render.Green},
|
||||||
|
{"Blue", render.RGBA(0, 153, 255, 255)},
|
||||||
|
{"Pink", render.Pink},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the SelectBox and populate its options.
|
||||||
|
sel := ui.NewSelectBox("Select 1", ui.Label{})
|
||||||
|
for _, option := range colors {
|
||||||
|
sel.AddItem(option.Label, option.Value, func() {
|
||||||
|
fmt.Printf("Picked option: %s\n", option.Value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// On change: set the window BG color.
|
||||||
|
sel.Handle(ui.Change, func(ed ui.EventData) error {
|
||||||
|
if val, ok := sel.GetValue(); ok {
|
||||||
|
if color, ok := val.Value.(render.Color); ok {
|
||||||
|
fmt.Printf("Set background to: %s\n", val.Label)
|
||||||
|
mw.SetBackground(color)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Not a valid color!")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("Not a valid SelectBox value!")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
frame.Pack(sel, ui.Pack{
|
||||||
|
Side: ui.W,
|
||||||
|
PadX: 4,
|
||||||
|
})
|
||||||
|
sel.Supervise(mw.Supervisor())
|
||||||
|
mw.Supervisor().Add(sel) // TODO: ideally Supervise() is all that's needed,
|
||||||
|
// but w/o this extra Add() the Button doesn't react.
|
||||||
|
}
|
||||||
|
|
||||||
|
mw.MainLoop()
|
||||||
|
}
|
31
glyphs.go
Normal file
31
glyphs.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
/*
|
||||||
|
Glyph images as Base64 encoded PNGs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// List of available glyphs.
|
||||||
|
const (
|
||||||
|
// Downward pointed black arrow 9x9 pixels.
|
||||||
|
GlyphDownArrow9x9 = `iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI
|
||||||
|
WXMAAC4jAAAuIwF4pT92AAAAKklEQVQY02NgoBZgZGBg+E+MIgYCChkZkTj/cRnCiCb4H4stWMF/
|
||||||
|
BpoBAGQSBQOpAugRAAAAAElFTkSuQmCC`
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetGlyph loads a PNG image from a hard-coded glyph.
|
||||||
|
func GetGlyph(b64 string) (image.Image, error) {
|
||||||
|
data, err := base64.StdEncoding.DecodeString(b64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bytes.NewReader(data)
|
||||||
|
return png.Decode(scanner)
|
||||||
|
}
|
14
image.go
14
image.go
|
@ -46,6 +46,20 @@ func NewImage(c Image) *Image {
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImageFromImage creates an Image from a Go standard library image.Image.
|
||||||
|
func ImageFromImage(e render.Engine, im image.Image) (*Image, error) {
|
||||||
|
tex, err := e.StoreTexture("imgbin.png", im)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Image{
|
||||||
|
Type: PNG,
|
||||||
|
Image: im,
|
||||||
|
texture: tex,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ImageFromTexture creates an Image from a texture.
|
// ImageFromTexture creates an Image from a texture.
|
||||||
func ImageFromTexture(tex render.Texturer) *Image {
|
func ImageFromTexture(tex render.Texturer) *Image {
|
||||||
return &Image{
|
return &Image{
|
||||||
|
|
178
selectbox.go
Normal file
178
selectbox.go
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/ui/theme"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SelectBox is a kind of MenuButton which allows choosing a value from a list.
|
||||||
|
type SelectBox struct {
|
||||||
|
MenuButton
|
||||||
|
|
||||||
|
name string
|
||||||
|
|
||||||
|
// Configurables after SelectBox creation.
|
||||||
|
AlwaysChange bool // always call the Change event, even if selection not changed.
|
||||||
|
|
||||||
|
// Child widgets specific to the SelectBox.
|
||||||
|
frame *Frame
|
||||||
|
label *Label
|
||||||
|
arrow *Image
|
||||||
|
|
||||||
|
// Data storage.
|
||||||
|
textVariable string
|
||||||
|
values []SelectValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectValue holds a mapping between a text label for a SelectBox and
|
||||||
|
// its underlying value (an arbitrary data type).
|
||||||
|
type SelectValue struct {
|
||||||
|
Label string
|
||||||
|
Value interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSelectBox creates a new SelectBox.
|
||||||
|
//
|
||||||
|
// The Label configuration passed in should be used to set font styles
|
||||||
|
// and padding; the Text, TextVariable and IntVariable of the Label will
|
||||||
|
// all be ignored, as SelectBox will handle the values internally.
|
||||||
|
func NewSelectBox(name string, withLabel Label) *SelectBox {
|
||||||
|
w := &SelectBox{
|
||||||
|
name: name,
|
||||||
|
textVariable: "Choose one",
|
||||||
|
values: []SelectValue{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the label has no text of its own.
|
||||||
|
withLabel.Text = ""
|
||||||
|
withLabel.TextVariable = &w.textVariable
|
||||||
|
withLabel.IntVariable = nil
|
||||||
|
|
||||||
|
w.frame = NewFrame(name + " Frame")
|
||||||
|
w.Button.child = w.frame
|
||||||
|
|
||||||
|
w.label = NewLabel(withLabel)
|
||||||
|
w.frame.Pack(w.label, Pack{
|
||||||
|
Side: W,
|
||||||
|
})
|
||||||
|
|
||||||
|
// arrow, _ := GetGlyph(GlyphDownArrow9x9)
|
||||||
|
// w.image = ImageFromImage(arrow, )
|
||||||
|
|
||||||
|
// Configure the button's appearance.
|
||||||
|
w.Button.Configure(Config{
|
||||||
|
BorderSize: 2,
|
||||||
|
BorderStyle: BorderSunken,
|
||||||
|
Background: render.White,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set sensible default padding on the label.
|
||||||
|
if w.label.Font.Padding == 0 && w.label.Font.PadX == 0 && w.label.Font.PadY == 0 {
|
||||||
|
w.label.Font.PadX = 4
|
||||||
|
w.label.Font.PadY = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
w.IDFunc(func() string {
|
||||||
|
return fmt.Sprintf("SelectBox<%s>", name)
|
||||||
|
})
|
||||||
|
|
||||||
|
w.setup()
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddItem adds a new option to the SelectBox's menu.
|
||||||
|
// The label is the text value to display.
|
||||||
|
// The value is the underlying value (string or int) for the TextVariable or IntVariable.
|
||||||
|
// The function callback runs when the option is picked.
|
||||||
|
func (w *SelectBox) AddItem(label string, value interface{}, f func()) {
|
||||||
|
// Add this label and its value mapping to the SelectBox.
|
||||||
|
w.values = append(w.values, SelectValue{
|
||||||
|
Label: label,
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Call the inherited MenuButton.AddItem.
|
||||||
|
w.MenuButton.AddItem(label, func() {
|
||||||
|
// Set the bound label.
|
||||||
|
var changed = w.textVariable != label
|
||||||
|
w.textVariable = label
|
||||||
|
|
||||||
|
if changed || w.AlwaysChange {
|
||||||
|
w.Event(Change, EventData{
|
||||||
|
Supervisor: w.MenuButton.supervisor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// If the current text label isn't in the options, pick
|
||||||
|
// the first option.
|
||||||
|
if _, ok := w.GetValue(); !ok {
|
||||||
|
w.textVariable = w.values[0].Label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: RemoveItem()
|
||||||
|
|
||||||
|
// Value returns the currently selected item in the SelectBox.
|
||||||
|
//
|
||||||
|
// Returns the SelectValue and true on success, and the Label or underlying Value
|
||||||
|
// can be read from the SelectValue struct. If no valid option is selected, the
|
||||||
|
// bool value returns false.
|
||||||
|
func (w *SelectBox) GetValue() (SelectValue, bool) {
|
||||||
|
for _, row := range w.values {
|
||||||
|
if w.textVariable == row.Label {
|
||||||
|
return row, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectValue{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute to re-evaluate the button state (in the case of radio buttons where
|
||||||
|
// a different button will affect the state of this one when clicked).
|
||||||
|
func (w *SelectBox) Compute(e render.Engine) {
|
||||||
|
w.MenuButton.Compute(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup the UI components and event handlers.
|
||||||
|
func (w *SelectBox) setup() {
|
||||||
|
w.Configure(Config{
|
||||||
|
BorderSize: 1,
|
||||||
|
BorderStyle: BorderSunken,
|
||||||
|
Background: theme.InputBackgroundColor,
|
||||||
|
})
|
||||||
|
|
||||||
|
w.Handle(MouseOver, func(ed EventData) error {
|
||||||
|
w.hovering = true
|
||||||
|
w.SetBackground(theme.ButtonHoverColor)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
w.Handle(MouseOut, func(ed EventData) error {
|
||||||
|
w.hovering = false
|
||||||
|
w.SetBackground(theme.InputBackgroundColor)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
w.Handle(MouseDown, func(ed EventData) error {
|
||||||
|
w.clicked = true
|
||||||
|
w.SetBackground(theme.ButtonBackgroundColor)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
w.Handle(MouseUp, func(ed EventData) error {
|
||||||
|
w.clicked = false
|
||||||
|
w.SetBackground(theme.InputBackgroundColor)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
w.Handle(Click, func(ed EventData) error {
|
||||||
|
// Are we properly configured?
|
||||||
|
if w.supervisor != nil && w.menu != nil {
|
||||||
|
w.menu.Show()
|
||||||
|
w.supervisor.PushModal(w.menu)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -37,6 +37,9 @@ const (
|
||||||
// Lifecycle event handlers.
|
// Lifecycle event handlers.
|
||||||
Compute // fired whenever the widget runs Compute
|
Compute // fired whenever the widget runs Compute
|
||||||
Present // fired whenever the widget runs Present
|
Present // fired whenever the widget runs Present
|
||||||
|
|
||||||
|
// Form field events.
|
||||||
|
Change
|
||||||
)
|
)
|
||||||
|
|
||||||
// EventData carries common data to event handlers.
|
// EventData carries common data to event handlers.
|
||||||
|
|
|
@ -10,6 +10,7 @@ var (
|
||||||
ButtonBackgroundColor = render.RGBA(200, 200, 200, 255)
|
ButtonBackgroundColor = render.RGBA(200, 200, 200, 255)
|
||||||
ButtonHoverColor = render.RGBA(200, 255, 255, 255)
|
ButtonHoverColor = render.RGBA(200, 255, 255, 255)
|
||||||
ButtonOutlineColor = render.Black
|
ButtonOutlineColor = render.Black
|
||||||
|
InputBackgroundColor = render.White
|
||||||
|
|
||||||
BorderColorOffset = 40
|
BorderColorOffset = 40
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user