Add SelectBox Widget

This commit is contained in:
Noah 2021-06-06 13:44:05 -07:00
parent 8f91971b62
commit 6df7bade48
7 changed files with 345 additions and 4 deletions

View File

@ -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
View 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
View 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)
}

View File

@ -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
View 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
})
}

View File

@ -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.

View File

@ -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
) )