From 6df7bade482ff97ebcffe02b2af5a3eb48612416 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sun, 6 Jun 2021 13:44:05 -0700 Subject: [PATCH] Add SelectBox Widget --- README.md | 8 +-- eg/forms/main.go | 114 ++++++++++++++++++++++++++++++ glyphs.go | 31 +++++++++ image.go | 14 ++++ selectbox.go | 178 +++++++++++++++++++++++++++++++++++++++++++++++ supervisor.go | 3 + theme/theme.go | 1 + 7 files changed, 345 insertions(+), 4 deletions(-) create mode 100644 eg/forms/main.go create mode 100644 glyphs.go create mode 100644 selectbox.go diff --git a/README.md b/README.md index c5dc2dd..8454e9d 100644 --- a/README.md +++ b/README.md @@ -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:** diff --git a/eg/forms/main.go b/eg/forms/main.go new file mode 100644 index 0000000..161f342 --- /dev/null +++ b/eg/forms/main.go @@ -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() +} diff --git a/glyphs.go b/glyphs.go new file mode 100644 index 0000000..5d6d296 --- /dev/null +++ b/glyphs.go @@ -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) +} \ No newline at end of file diff --git a/image.go b/image.go index 69e331f..700613c 100644 --- a/image.go +++ b/image.go @@ -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{ diff --git a/selectbox.go b/selectbox.go new file mode 100644 index 0000000..4ebae5f --- /dev/null +++ b/selectbox.go @@ -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 + }) +} + diff --git a/supervisor.go b/supervisor.go index b4c1d0d..202695e 100644 --- a/supervisor.go +++ b/supervisor.go @@ -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. diff --git a/theme/theme.go b/theme/theme.go index d0df939..eabcfa3 100644 --- a/theme/theme.go +++ b/theme/theme.go @@ -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 )