Browse Source

Add SelectBox Widget

master
Noah Petherbridge 2 months ago
parent
commit
6df7bade48
7 changed files with 345 additions and 4 deletions
  1. +4
    -4
      README.md
  2. +114
    -0
      eg/forms/main.go
  3. +31
    -0
      glyphs.go
  4. +14
    -0
      image.go
  5. +178
    -0
      selectbox.go
  6. +3
    -0
      supervisor.go
  7. +1
    -0
      theme/theme.go

+ 4
- 4
README.md View File

@@ -8,14 +8,14 @@ applications (SDL2, for Linux, MacOS and Windows) as well as web browsers

![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.)_

It is very much a **work in progress** and may contain bugs and its API may
change as bugs are fixed or features added.

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
the SDL2 and Canvas back-ends.
(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.
* [x] **Menu**: a frame full of clickable links and separators. Usually used as
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:**

* [ ] **Scrollbar**: a Frame including a trough, scroll buttons and a
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:**



+ 114
- 0
eg/forms/main.go 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
- 0
glyphs.go 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)
}

+ 14
- 0
image.go View File

@@ -46,6 +46,20 @@ func NewImage(c Image) *Image {
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.
func ImageFromTexture(tex render.Texturer) *Image {
return &Image{


+ 178
- 0
selectbox.go 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
})
}


+ 3
- 0
supervisor.go View File

@@ -37,6 +37,9 @@ const (
// Lifecycle event handlers.
Compute // fired whenever the widget runs Compute
Present // fired whenever the widget runs Present

// Form field events.
Change
)

// EventData carries common data to event handlers.


+ 1
- 0
theme/theme.go View File

@@ -10,6 +10,7 @@ var (
ButtonBackgroundColor = render.RGBA(200, 200, 200, 255)
ButtonHoverColor = render.RGBA(200, 255, 255, 255)
ButtonOutlineColor = render.Black
InputBackgroundColor = render.White

BorderColorOffset = 40
)


Loading…
Cancel
Save