ListBox, ScrollBar, Magic Form and forms demo

This commit is contained in:
Noah 2023-04-08 21:18:24 -07:00
parent d82ef0b751
commit 8716c479e9
14 changed files with 1456 additions and 129 deletions

View File

@ -100,13 +100,15 @@ most complex.
**Fully implemented widgets:**
In order of simplicity:
* [x] **BaseWidget**: the base class of all Widgets.
* The `Widget` interface describes the functions common to all Widgets,
such as SetBackground, Configure, MoveTo, Resize, and so on.
* BaseWidget provides sane default implementations for all the methods
required by the Widget interface. Most Widgets inherit from
the BaseWidget.
* [x] **Frame**: a layout wrapper for other widgets.
the BaseWidget and override what they need.
* [x] **Frame**: a layout wrapper for child widgets.
* Pack() lets you add child widgets to the Frame, aligned against one side
or another, and ability to expand widgets to take up remaining space in
their part of the Frame.
@ -153,6 +155,10 @@ most complex.
a modal pop-up by the MenuButton and MenuBar. [Example](eg/menus)
* [x] **SelectBox**: a kind of MenuButton that lets the user choose a
value from a list of possible values.
* [x] **Scrollbar**: a Frame including a trough, scroll buttons and a
draggable slider.
* [x] **ListBox**: a multi-line select box with a ScrollBar that can hold arbitrary
child widgets (usually Labels which have a shortcut function for).
Some useful helper widgets:
@ -161,16 +167,11 @@ Some useful helper widgets:
custom hexadecimal value by hand (needs assistance from your program).
[Example](eg/colorpicker)
**Work in progress widgets:**
* [ ] **Scrollbar**: a Frame including a trough, scroll buttons and a
draggable slider.
**Wish list for the longer-term future:**
**Planned widgets:**
* [ ] **TextBox:** an editable text field that the user can focus and type
a value into.
* Would depend on the WindowManager to manage focus for the widgets.
* [ ] **TextArea:** an editable multi-line text field with a scrollbar.
## Supervisor for Interaction

View File

@ -3,17 +3,13 @@
Here are some example programs using go/ui, each accompanied by a
screenshot and description:
* [Hello, World!](hello-world/): a basic UI demo with a Label and a
Button.
* [Frame Place()](frame-place/): demonstrates using the Place() layout
management option for Frame widgets.
* [Window Manager](windows/): demonstrates the Window widget and window
management features of the Supervisor.
* [Tooltip](tooltip/): demonstrates the Tooltip widget on a variety of buttons
scattered around the window.
* [Hello, World!](hello-world/): a basic UI demo with a Label and a Button.
* [Frame Placement](frame-place/): demonstrates using the Place() layout management option for Frame widgets.
* [Window Manager](windows/): demonstrates the Window widget and window management features of the Supervisor.
* [Forms](forms/): demonstrates some form controls and the `magicform` helper module for building forms quickly.
* [Tooltip](tooltip/): demonstrates the Tooltip widget on a variety of buttons scattered around the window.
* [Menus](menus/): demonstrates various Menu Buttons and a Menu Bar.
* [Themes](themes/): a UI demo that shows off the Default, Flat, and Dark UI
themes as part of experimental theming support.
* [Themes](themes/): a UI demo that shows off the Default, Flat, and Dark UI themes as part of experimental theming support.
* [TabFrame](tabframe/): demo for the TabFrame widget showing multiple Windows
with tabbed interfaces.
* [ColorPicker](colorpicker/): demo for the ColorPicker widget.

9
eg/forms/README.md Normal file
View File

@ -0,0 +1,9 @@
# Forms
A demonstration of form controls in `go/ui` and example how to use the `magicform` helper module.
```bash
go run main.go
```
![Screenshot](screenshot.png)

View File

@ -6,109 +6,280 @@ import (
"git.kirsle.net/go/render"
"git.kirsle.net/go/render/sdl"
"git.kirsle.net/go/ui"
"git.kirsle.net/go/ui/magicform"
"git.kirsle.net/go/ui/style"
)
func init() {
sdl.DefaultFontFilename = "../DejaVuSans.ttf"
}
var (
MenuFont = render.Text{
Size: 12,
PadX: 4,
PadY: 2,
}
TabFont = render.Text{
Size: 12,
PadX: 4,
PadY: 2,
}
)
var ButtonStylePrimary = &style.Button{
Background: render.RGBA(0, 60, 153, 255),
Foreground: render.White,
HoverBackground: render.RGBA(0, 153, 255, 255),
HoverForeground: render.White,
OutlineColor: render.DarkGrey,
OutlineSize: 1,
BorderStyle: style.BorderRaised,
BorderSize: 2,
}
func main() {
mw, err := ui.NewMainWindow("Forms Test")
mw, err := ui.NewMainWindow("Forms Test", 500, 375)
if err != nil {
panic(err)
}
mw.SetBackground(render.White)
// Tabbed UI.
tabFrame := ui.NewTabFrame("Tabs")
makeAppFrame(mw, tabFrame)
makeAboutFrame(mw, tabFrame)
// 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.
}
tabFrame.Supervise(mw.Supervisor())
mw.Pack(tabFrame, ui.Pack{
Side: ui.N,
Expand: true,
Padding: 10,
})
mw.SetBackground(render.Grey)
mw.MainLoop()
}
func makeAppFrame(mw *ui.MainWindow, tf *ui.TabFrame) *ui.Frame {
frame := tf.AddTab("Index", ui.NewLabel(ui.Label{
Text: "Form Controls",
Font: TabFont,
}))
// Form variables
var (
bgcolor = render.Grey
letter string
checkBool1 bool
checkBool2 = true
pagerLabel = "Page 1 of 20"
)
// Magic Form is a handy module for easily laying out forms of widgets.
form := magicform.Form{
Supervisor: mw.Supervisor(),
Engine: mw.Engine,
Vertical: true,
LabelWidth: 120,
PadY: 2,
PadX: 8,
}
// You add to it a list of fields which support all sorts of different
// form control types.
fields := []magicform.Field{
// Simple text sections - you can write paragraphs or use a bold font
// to make section labels that span the full width of your frame.
{
Label: "Checkbox controls bound to bool values:",
Font: MenuFont,
},
// Checkbox widgets: just bind a BoolVariable and this row will draw
// with a checkbox next to a label.
{
Label: "Check this box to toggle a boolean",
Font: MenuFont,
BoolVariable: &checkBool1,
OnClick: func() {
fmt.Printf("The checkbox was clicked! Value is now: %+v\n", checkBool1)
},
},
{
Label: "Uncheck this one",
Font: MenuFont,
BoolVariable: &checkBool2,
OnClick: func() {
fmt.Printf("The checkbox was clicked! Value is now: %+v\n", checkBool1)
},
},
// SelectBox widgets: just bind a SelectValue and provide Options and
// it will draw with a label (LabelWidth wide) next to a SelectBox button.
{
Label: "Window color:",
Font: MenuFont,
SelectValue: &bgcolor,
Options: []magicform.Option{
{
Label: "Grey",
Value: render.Grey,
},
{
Label: "White",
Value: render.White,
},
{
Label: "Yellow",
Value: render.Yellow,
},
{
Label: "Cyan",
Value: render.Cyan,
},
{
Label: "Green",
Value: render.Green,
},
{
Label: "Blue",
Value: render.RGBA(0, 153, 255, 255),
},
{
Label: "Pink",
Value: render.Pink,
},
},
OnSelect: func(v interface{}) {
value, _ := v.(render.Color)
mw.SetBackground(value)
},
},
// ListBox widgets
{
Type: magicform.Listbox,
Label: "Favorite letter:",
Font: MenuFont,
SelectValue: &letter,
Options: []magicform.Option{
{Label: "A is for apple", Value: "A"},
{Label: "B is for boy", Value: "B"},
{Label: "C is for cat", Value: "C"},
{Label: "D is for dog", Value: "D"},
{Label: "E is for elephant", Value: "E"},
{Label: "F is for far", Value: "F"},
{Label: "G is for ghost", Value: "G"},
{Label: "H is for high", Value: "H"},
{Label: "I is for inside", Value: "I"},
{Label: "J is for joker", Value: "J"},
{Label: "K is for kangaroo", Value: "K"},
{Label: "L is for lion", Value: "L"},
{Label: "M is for mouse", Value: "M"},
{Label: "N is for night", Value: "N"},
{Label: "O is for over", Value: "O"},
{Label: "P is for parry", Value: "P"},
{Label: "Q is for quarry", Value: "Q"},
{Label: "R is for reality", Value: "R"},
{Label: "S is for sunshine", Value: "S"},
{Label: "T is for tree", Value: "T"},
{Label: "U is for under", Value: "U"},
{Label: "V is for vehicle", Value: "V"},
{Label: "W is for watermelon", Value: "W"},
{Label: "X is for xylophone", Value: "X"},
{Label: "Y is for yellow", Value: "Y"},
{Label: "Z is for zebra", Value: "Z"},
},
OnSelect: func(v interface{}) {
value, _ := v.(string)
fmt.Printf("You clicked on: %s\n", value)
},
},
// Pager rows to show an easy paginated UI.
// TODO: this is currently broken and Supervisor doesn't pick it up
{
Label: "A paginator when you need one. You can limit MaxPageButtons\n" +
"and the right arrow can keep selecting past the last page.",
},
{
LabelVariable: &pagerLabel,
Label: "Page:",
Pager: ui.NewPager(ui.Pager{
Page: 1,
Pages: 20,
PerPage: 10,
MaxPageButtons: 8,
Font: MenuFont,
OnChange: func(page, perPage int) {
fmt.Printf("Pager clicked: page=%d perPage=%d\n", page, perPage)
pagerLabel = fmt.Sprintf("Page %d of %d", page, 20)
},
}),
},
// Simple variable bindings.
{
Type: magicform.Value,
Label: "The first bool var:",
TextVariable: &pagerLabel,
},
// Buttons for the bottom of your form.
{
Buttons: []magicform.Field{
{
Label: "Save",
ButtonStyle: ButtonStylePrimary,
Font: MenuFont,
OnClick: func() {
fmt.Println("Primary button clicked")
},
},
{
Label: "Cancel",
Font: MenuFont,
OnClick: func() {
fmt.Println("Secondary button clicked")
},
},
},
},
}
form.Create(frame, fields)
return frame
}
func makeAboutFrame(mw *ui.MainWindow, tf *ui.TabFrame) *ui.Frame {
frame := tf.AddTab("About", ui.NewLabel(ui.Label{
Text: "About",
Font: TabFont,
}))
form := magicform.Form{
Supervisor: mw.Supervisor(),
Engine: mw.Engine,
Vertical: true,
LabelWidth: 120,
PadY: 2,
PadX: 8,
}
fields := []magicform.Field{
{
Label: "About",
Font: MenuFont,
},
{
Label: "This example shows off the UI toolkit's use for form controls,\n" +
"and how the magicform helper module can make simple forms\n" +
"easy to compose quickly.",
Font: MenuFont,
},
}
form.Create(frame, fields)
return frame
}

BIN
eg/forms/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -12,6 +12,7 @@ func AbsolutePosition(w Widget) render.Point {
var (
node = w
ok bool
pt render.Point
)
for {
@ -20,7 +21,9 @@ func AbsolutePosition(w Widget) render.Point {
return abs
}
abs.Add(node.Point())
pt = node.Point()
pt.Add(render.NewPoint(node.BorderSize(), node.BorderSize()))
abs.Add(pt)
}
}

6
go.mod
View File

@ -3,7 +3,7 @@ module git.kirsle.net/go/ui
go 1.16
require (
git.kirsle.net/go/render v0.0.0-20211231003948-9e640ab5c3da
github.com/veandco/go-sdl2 v0.4.8 // indirect
golang.org/x/image v0.0.0-20211028202545-6944b10bf410
git.kirsle.net/go/render v0.0.0-20220505053906-129a24300dfa
github.com/veandco/go-sdl2 v0.4.33 // indirect
golang.org/x/image v0.6.0
)

35
go.sum
View File

@ -4,11 +4,18 @@ git.kirsle.net/go/render v0.0.0-20210614025954-d77f5056b782 h1:Ko+NvZxmJbW+M1dA2
git.kirsle.net/go/render v0.0.0-20210614025954-d77f5056b782/go.mod h1:ss7pvZbGWrMaDuZwyUTjV9+T0AJwAkxZZHwMFsvHrkk=
git.kirsle.net/go/render v0.0.0-20211231003948-9e640ab5c3da h1:wbeh/hHiwmXqf/3VPrbE/PADTcT1niQWhxxK81Ize3o=
git.kirsle.net/go/render v0.0.0-20211231003948-9e640ab5c3da/go.mod h1:ss7pvZbGWrMaDuZwyUTjV9+T0AJwAkxZZHwMFsvHrkk=
git.kirsle.net/go/render v0.0.0-20220505053906-129a24300dfa h1:Oa99SXkmFGnUNy+toPMQyW/eYotN1nZ9BWAThQ/huiM=
git.kirsle.net/go/render v0.0.0-20220505053906-129a24300dfa/go.mod h1:ss7pvZbGWrMaDuZwyUTjV9+T0AJwAkxZZHwMFsvHrkk=
github.com/veandco/go-sdl2 v0.4.1/go.mod h1:FB+kTpX9YTE+urhYiClnRzpOXbiWgaU3+5F2AB78DPg=
github.com/veandco/go-sdl2 v0.4.7 h1:VfpCM+LfEGDbHdByglCo2bcBsevjFvzl8W0f6VLNitg=
github.com/veandco/go-sdl2 v0.4.7/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
github.com/veandco/go-sdl2 v0.4.8 h1:A26KeX6R1CGt/BQGEov6oxYmVGMMEWDVqTvK1tXvahE=
github.com/veandco/go-sdl2 v0.4.8/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
github.com/veandco/go-sdl2 v0.4.33 h1:cxQ0OdUBEByHxvCyrGxy9F8WpL38Ya6hzV4n27QL84M=
github.com/veandco/go-sdl2 v0.4.33/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e h1:PzJMNfFQx+QO9hrC1GwZ4BoPGeNGhfeQEgcQFArEjPk=
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@ -16,6 +23,34 @@ golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jp
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

309
listbox.go Normal file
View File

@ -0,0 +1,309 @@
package ui
import (
"fmt"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui/style"
)
// ListBox is a selectable list of values like a multi-line SelectBox.
type ListBox struct {
*Frame
name string
children []*ListValue
style *style.ListBox
supervisor *Supervisor
list *Frame
scrollbar *ScrollBar
scrollFraction float64
maxHeight int
// Variable bindings: give these pointers to your values.
Variable interface{} // pointer to e.g. a string or int
// TextVariable *string // string value
// IntVariable *int // integer value
}
// ListValue is an item in the ListBox. It has an arbitrary widget as a
// "label" (usually a Label) and a value (string or int) when it's "selected"
type ListValue struct {
Frame *Frame
Label Widget
Value interface{}
}
// NewListBox creates a new ListBox.
func NewListBox(name string, config ListBox) *ListBox {
w := &ListBox{
Frame: NewFrame(name + " Frame"),
list: NewFrame(name + " List"),
name: name,
children: []*ListValue{},
Variable: config.Variable,
// TextVariable: config.TextVariable,
// IntVariable: config.IntVariable,
style: &style.DefaultListBox,
}
// if config.Width > 0 && config.Height > 0 {
// w.Frame.Resize(render.NewRect(config.Width, config.Height))
// }
w.IDFunc(func() string {
return fmt.Sprintf("ListBox<%s>", name)
})
w.SetStyle(Theme.ListBox)
w.setup()
return w
}
// SetStyle sets the listbox style.
func (w *ListBox) SetStyle(v *style.ListBox) {
if v == nil {
v = &style.DefaultListBox
}
w.style = v
fmt.Printf("set style: %+v\n", v)
w.Frame.Configure(Config{
BorderSize: w.style.BorderSize,
BorderStyle: BorderStyle(w.style.BorderStyle),
Background: w.style.Background,
})
// If the child is a Label, apply the foreground color.
// if label, ok := w.child.(*Label); ok {
// label.Font.Color = w.style.Foreground
// }
}
// GetStyle gets the listbox style.
func (w *ListBox) GetStyle() *style.ListBox {
return w.style
}
// Supervise the ListBox. This is necessary for granting mouse-over events
// to the items in the list.
func (w *ListBox) Supervise(s *Supervisor) {
w.supervisor = s
w.scrollbar.Supervise(s)
// Add all the list items to be supervised.
for _, c := range w.children {
w.supervisor.Add(c.Frame)
}
}
// AddLabel adds a simple text-based label to the Listbox.
// 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 *ListBox) AddLabel(label string, value interface{}, f func()) {
row := NewFrame(label + " Frame")
child := NewLabel(Label{
Text: label,
Font: render.Text{
Color: w.style.Foreground,
Size: 11,
Padding: 2,
},
})
row.Pack(child, Pack{
Side: W,
FillX: true,
})
// Add this label and its value mapping to the ListBox.
w.children = append(w.children, &ListValue{
Frame: row,
Label: child,
Value: value,
})
// Event handlers for the item row.
// row.Handle(MouseOver, func(ed EventData) error {
// if ed.Point.Inside(AbsoluteRect(w.scrollbar)) {
// return nil // ignore if over scrollbar
// }
// row.SetBackground(w.style.HoverBackground)
// child.Font.Color = w.style.HoverForeground
// return nil
// })
row.Handle(MouseMove, func(ed EventData) error {
if ed.Point.Inside(AbsoluteRect(w.scrollbar)) {
// we wandered onto the scrollbar, cancel mouseover
return row.Event(MouseOut, ed)
}
row.SetBackground(w.style.HoverBackground)
child.Font.Color = w.style.HoverForeground
return nil
})
row.Handle(MouseOut, func(ed EventData) error {
if cur, ok := w.GetValue(); ok && cur == value {
row.SetBackground(w.style.SelectedBackground)
child.Font.Color = w.style.SelectedForeground
} else {
fmt.Printf("couldn't get value? %+v %+v\n", cur, ok)
row.SetBackground(w.style.Background)
child.Font.Color = w.style.Foreground
}
return nil
})
row.Handle(MouseUp, func(ed EventData) error {
if cur, ok := w.GetValue(); ok && cur == value {
row.SetBackground(w.style.SelectedBackground)
child.Font.Color = w.style.SelectedForeground
} else {
row.SetBackground(w.style.Background)
child.Font.Color = w.style.Foreground
}
return nil
})
row.Handle(Click, func(ed EventData) error {
// Trigger if we are not hovering over the (overlapping) scrollbar.
if !ed.Point.Inside(AbsoluteRect(w.scrollbar)) {
w.Event(Change, EventData{
Supervisor: w.supervisor,
Value: value,
})
}
return nil
})
// Append the item into the ListBox frame.
w.Frame.Pack(row, Pack{
Side: N,
PadY: 1,
Fill: true,
})
// If the current text label isn't in the options, pick
// the first option.
if _, ok := w.GetValue(); !ok {
w.Variable = w.children[0].Value
row.SetBackground(w.style.SelectedBackground)
}
}
// TODO: RemoveItem()
// GetValue returns the currently selected item in the ListBox.
//
// 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 *ListBox) GetValue() (*ListValue, bool) {
for _, row := range w.children {
if w.Variable != nil && w.Variable == row.Value {
return row, true
}
}
return nil, false
}
// SetValueByLabel sets the currently selected option to the given label.
func (w *ListBox) SetValueByLabel(label string) bool {
for _, option := range w.children {
if child, ok := option.Label.(*Label); ok && child.Text == label {
w.Variable = option.Value
return true
}
}
return false
}
// SetValue sets the currently selected option to the given value.
func (w *ListBox) SetValue(value interface{}) bool {
w.Variable = value
for _, option := range w.children {
if option.Value == value {
w.Variable = option.Value
return true
}
}
return 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 *ListBox) Compute(e render.Engine) {
w.computeVisible()
w.Frame.Compute(e)
}
// setup the UI components and event handlers.
func (w *ListBox) setup() {
// w.Configure(Config{
// BorderSize: 1,
// BorderStyle: BorderSunken,
// Background: theme.InputBackgroundColor,
// })
w.scrollbar = NewScrollBar(ScrollBar{})
w.scrollbar.Handle(Scroll, func(ed EventData) error {
fmt.Printf("Scroll event: %f%% unit %d\n", ed.ScrollFraction*100, ed.ScrollUnits)
w.scrollFraction = ed.ScrollFraction
return nil
})
w.Frame.Pack(w.scrollbar, Pack{
Side: E,
FillY: true,
Padding: 0,
})
// w.Frame.Pack(w.list, Pack{
// Side: E,
// FillY: true,
// Expand: true,
// })
}
// Compute which items of the list should be visible based on scroll position.
func (w *ListBox) computeVisible() {
if len(w.children) == 0 {
return
}
// Sample the first element's height.
var (
myHeight = w.height
maxTop = w.maxHeight - myHeight + w.children[len(w.children)-1].Frame.height
top = int(w.scrollFraction * float64(maxTop))
// itemHeight = w.children[0].Label.Size().H
)
var (
scan int
scrollFreed int
totalHeight int
)
for _, c := range w.children {
childHeight := c.Frame.Size().H + 2
if top > 0 && scan+childHeight < top {
scrollFreed += childHeight
c.Frame.Hide()
} else if scan+childHeight > myHeight+scrollFreed {
c.Frame.Hide()
} else {
c.Frame.Show()
}
scan += childHeight // for padding
totalHeight += childHeight
}
w.maxHeight = totalHeight
}
func (w *ListBox) Present(e render.Engine, p render.Point) {
w.Frame.Present(e, p)
// HACK to get the scrollbar to appear on top of the list frame :(
pos := AbsolutePosition(w.scrollbar)
// pos.X += w.BoxThickness(w.style.BorderSize / 2) // HACK
w.scrollbar.Present(e, pos)
}

503
magicform/magicform.go Normal file
View File

@ -0,0 +1,503 @@
// Package magicform helps create simple form layouts with go/ui.
package magicform
import (
"fmt"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
"git.kirsle.net/go/ui/style"
)
type Type int
const (
Auto Type = iota
Text // free, wide Label row
Frame // custom frame from the caller
Button // Single button with a label
Value // a Label & Value row (value not editable)
Textbox
Checkbox
Radiobox
Selectbox
Listbox
Color
Pager
)
// Form configuration.
type Form struct {
Supervisor *ui.Supervisor // Required for most useful forms
Engine render.Engine
// For vertical forms.
Vertical bool
LabelWidth int // size of left frame for labels.
PadY int // spacer between (vertical) forms
PadX int
}
/*
Field for your form (or form-aligned label sections, etc.)
The type of Form control to render is inferred based on bound
variables and other configuration.
*/
type Field struct {
// Type may be inferred by presence of other params.
Type Type
// Set a text string and font for simple labels or paragraphs.
Label string
LabelVariable *string // a TextVariable to drive the Label
Font render.Text
// Easy button row: make Buttons an array of Button fields
Buttons []Field
ButtonStyle *style.Button
// Easy Paginator. DO NOT SUPERVISE, let the Create do so!
Pager *ui.Pager
// If you send a *ui.Frame to insert, the Type is inferred
// to be Frame.
Frame *ui.Frame
// Variable bindings, the type may infer to be:
BoolVariable *bool // Checkbox
TextVariable *string // Textbox
IntVariable *int // Textbox
Options []Option // Selectbox
SelectValue interface{} // Selectbox default choice
Color *render.Color // Color
Readonly bool // draw the value as a flat label
// For text-type fields, opt-in to let magicform prompt the
// user using the game's developer shell.
PromptUser func(answer string)
// Tooltip to add to a form control.
// Checkbox only for now.
Tooltip ui.Tooltip // config for the tooltip only
// Handlers you can configure
OnSelect func(value interface{}) // Selectbox
OnClick func() // Button
}
// Option used in Selectbox or Radiobox fields.
type Option struct {
Value interface{}
Label string
Separator bool
}
/*
Create the form field and populate it into the given Frame.
Renders the form vertically.
*/
func (form Form) Create(into *ui.Frame, fields []Field) {
for n, row := range fields {
row := row
if row.Frame != nil {
into.Pack(row.Frame, ui.Pack{
Side: ui.N,
FillX: true,
})
continue
}
frame := ui.NewFrame(fmt.Sprintf("Line %d", n))
into.Pack(frame, ui.Pack{
Side: ui.N,
FillX: true,
PadY: form.PadY,
})
// Buttons row?
if row.Buttons != nil && len(row.Buttons) > 0 {
for _, row := range row.Buttons {
row := row
btn := ui.NewButton(row.Label, ui.NewLabel(ui.Label{
Text: row.Label,
Font: row.Font,
}))
if row.ButtonStyle != nil {
btn.SetStyle(row.ButtonStyle)
}
btn.Handle(ui.Click, func(ed ui.EventData) error {
if row.OnClick != nil {
row.OnClick()
} else {
return fmt.Errorf("no OnClick handler for button %s", row.Label)
}
return nil
})
btn.Compute(form.Engine)
form.Supervisor.Add(btn)
// Tooltip? TODO - make nicer.
if row.Tooltip.Text != "" || row.Tooltip.TextVariable != nil {
tt := ui.NewTooltip(btn, row.Tooltip)
tt.Supervise(form.Supervisor)
}
frame.Pack(btn, ui.Pack{
Side: ui.W,
PadX: 2,
PadY: 2,
})
}
continue
}
// Infer the type of the form field.
if row.Type == Auto {
row.Type = row.Infer()
if row.Type == Auto {
continue
}
}
// Is there a label frame to the left?
// - Checkbox gets a full row.
fmt.Printf("Label=%+v Var=%+v\n", row.Label, row.LabelVariable)
if (row.Label != "" || row.LabelVariable != nil) && row.Type != Checkbox {
labFrame := ui.NewFrame("Label Frame")
labFrame.Configure(ui.Config{
Width: form.LabelWidth,
})
frame.Pack(labFrame, ui.Pack{
Side: ui.W,
})
// Draw the label text into it.
label := ui.NewLabel(ui.Label{
Text: row.Label,
TextVariable: row.LabelVariable,
Font: row.Font,
})
labFrame.Pack(label, ui.Pack{
Side: ui.W,
})
}
// Pager row?
if row.Pager != nil {
row.Pager.Supervise(form.Supervisor)
frame.Pack(row.Pager, ui.Pack{
Side: ui.W,
})
}
// Simple "Value" row with a Label to its left.
if row.Type == Value {
lbl := ui.NewLabel(ui.Label{
Text: row.Label,
Font: row.Font,
TextVariable: row.TextVariable,
IntVariable: row.IntVariable,
})
frame.Pack(lbl, ui.Pack{
Side: ui.W,
FillX: true,
Expand: true,
})
// Tooltip? TODO - make nicer.
if row.Tooltip.Text != "" || row.Tooltip.TextVariable != nil {
tt := ui.NewTooltip(lbl, row.Tooltip)
tt.Supervise(form.Supervisor)
}
}
// Color picker button.
if row.Type == Color && row.Color != nil {
btn := ui.NewButton("ColorPicker", ui.NewLabel(ui.Label{
Text: " ",
Font: row.Font,
}))
style := style.DefaultButton
style.Background = *row.Color
style.HoverBackground = style.Background.Lighten(20)
btn.SetStyle(&style)
form.Supervisor.Add(btn)
frame.Pack(btn, ui.Pack{
Side: ui.W,
FillX: true,
Expand: true,
})
btn.Handle(ui.Click, func(ed ui.EventData) error {
// Open a ColorPicker widget.
picker, err := ui.NewColorPicker(ui.ColorPicker{
Title: "Select a color",
Supervisor: form.Supervisor,
Engine: form.Engine,
Color: *row.Color,
OnManualInput: func(callback func(render.Color)) {
// TODO: prompt for color
},
})
if err != nil {
return err
}
picker.Then(func(color render.Color) {
*row.Color = color
style.Background = color
style.HoverBackground = style.Background.Lighten(20)
// call onClick to save change to disk now
if row.OnClick != nil {
row.OnClick()
}
})
picker.Center(form.Engine.WindowSize())
picker.Show()
return nil
})
}
// Buttons and Text fields (for now).
if row.Type == Button || row.Type == Textbox {
btn := ui.NewButton("Button", ui.NewLabel(ui.Label{
Text: row.Label,
Font: row.Font,
TextVariable: row.TextVariable,
IntVariable: row.IntVariable,
}))
frame.Pack(btn, ui.Pack{
Side: ui.W,
FillX: true,
Expand: true,
})
// Not clickable if Readonly.
if !row.Readonly {
form.Supervisor.Add(btn)
}
// Tooltip? TODO - make nicer.
if row.Tooltip.Text != "" || row.Tooltip.TextVariable != nil {
tt := ui.NewTooltip(btn, row.Tooltip)
tt.Supervise(form.Supervisor)
}
// Handlers
btn.Handle(ui.Click, func(ed ui.EventData) error {
// Text boxes, we want to prompt the user to enter new value?
if row.PromptUser != nil {
var value string
if row.TextVariable != nil {
value = *row.TextVariable
} else if row.IntVariable != nil {
value = fmt.Sprintf("%d", *row.IntVariable)
}
// TODO: prompt user for new value
_ = value
// shmem.PromptPre("Enter new value: ", value, func(answer string) {
// if answer != "" {
// row.PromptUser(answer)
// }
// })
}
if row.OnClick != nil {
row.OnClick()
}
return nil
})
}
// Checkbox?
if row.Type == Checkbox {
cb := ui.NewCheckbox("Checkbox", row.BoolVariable, ui.NewLabel(ui.Label{
Text: row.Label,
Font: row.Font,
}))
cb.Supervise(form.Supervisor)
frame.Pack(cb, ui.Pack{
Side: ui.W,
FillX: true,
})
// Tooltip? TODO - make nicer.
if row.Tooltip.Text != "" || row.Tooltip.TextVariable != nil {
tt := ui.NewTooltip(cb, row.Tooltip)
tt.Supervise(form.Supervisor)
}
// Handlers
cb.Handle(ui.Click, func(ed ui.EventData) error {
if row.OnClick != nil {
row.OnClick()
}
return nil
})
}
// Selectbox? also Radiobox for now.
if row.Type == Selectbox || row.Type == Radiobox {
btn := ui.NewSelectBox("Select", ui.Label{
Font: row.Font,
})
frame.Pack(btn, ui.Pack{
Side: ui.W,
FillX: true,
Expand: true,
})
if row.Options != nil {
for _, option := range row.Options {
if option.Separator {
btn.AddSeparator()
continue
}
btn.AddItem(option.Label, option.Value, func() {})
}
}
if row.SelectValue != nil {
btn.SetValue(row.SelectValue)
}
btn.Handle(ui.Change, func(ed ui.EventData) error {
if selection, ok := btn.GetValue(); ok {
if row.OnSelect != nil {
row.OnSelect(selection.Value)
}
// Update bound variables.
if v, ok := selection.Value.(int); ok && row.IntVariable != nil {
*row.IntVariable = v
}
if v, ok := selection.Value.(string); ok && row.TextVariable != nil {
*row.TextVariable = v
}
}
return nil
})
// Tooltip? TODO - make nicer.
if row.Tooltip.Text != "" || row.Tooltip.TextVariable != nil {
tt := ui.NewTooltip(btn, row.Tooltip)
tt.Supervise(form.Supervisor)
}
btn.Supervise(form.Supervisor)
form.Supervisor.Add(btn)
}
// ListBox?
if row.Type == Listbox {
btn := ui.NewListBox("List", ui.ListBox{
Variable: row.SelectValue,
})
btn.Configure(ui.Config{
Height: 120,
})
frame.Pack(btn, ui.Pack{
Side: ui.W,
FillX: true,
Expand: true,
})
if row.Options != nil {
for _, option := range row.Options {
if option.Separator {
// btn.AddSeparator()
continue
}
fmt.Printf("LISTBOX: Insert label '%s' with value %+v\n", option.Label, option.Value)
btn.AddLabel(option.Label, option.Value, func() {})
}
}
if row.SelectValue != nil {
fmt.Printf("LISTBOX: Set value to %s\n", row.SelectValue)
btn.SetValue(row.SelectValue)
}
btn.Handle(ui.Change, func(ed ui.EventData) error {
if selection, ok := btn.GetValue(); ok {
if row.OnSelect != nil {
row.OnSelect(selection.Value)
}
// Update bound variables.
if v, ok := selection.Value.(int); ok && row.IntVariable != nil {
*row.IntVariable = v
}
if v, ok := selection.Value.(string); ok && row.TextVariable != nil {
*row.TextVariable = v
}
}
return nil
})
// Tooltip? TODO - make nicer.
if row.Tooltip.Text != "" || row.Tooltip.TextVariable != nil {
tt := ui.NewTooltip(btn, row.Tooltip)
tt.Supervise(form.Supervisor)
}
btn.Supervise(form.Supervisor)
// form.Supervisor.Add(btn) // for btn.Handle(Change) to work??
}
}
}
/*
Infer the type if the field was of type Auto.
Returns the first Type inferred from the field by checking in
this order:
- Frame if the field has a *Frame
- Checkbox if there is a *BoolVariable
- Selectbox if there are Options
- Textbox if there is a *TextVariable
- Text if there is a Label
May return Auto if none of the above and be ignored.
*/
func (field Field) Infer() Type {
if field.Frame != nil {
return Frame
}
if field.BoolVariable != nil {
return Checkbox
}
if field.Options != nil && len(field.Options) > 0 {
return Selectbox
}
if field.TextVariable != nil || field.IntVariable != nil {
return Textbox
}
if field.Label != "" {
return Text
}
if field.Pager != nil {
return Pager
}
return Auto
}

254
scrollbar.go Normal file
View File

@ -0,0 +1,254 @@
package ui
import (
"fmt"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui/style"
)
// Scrollbar dimensions, TODO: make configurable.
var (
scrollWidth = 20
scrollbarHeight = 40
)
// ScrollBar is a classic scrolling widget.
type ScrollBar struct {
*Frame
style *style.Button
supervisor *Supervisor
trough *Frame
slider *Frame
// Configurable scroll ranges.
Min int
Max int
Step int
value int
// Variable bindings: give these pointers to your values.
Variable interface{} // pointer to e.g. a string or int
// TextVariable *string // string value
// IntVariable *int // integer value
// Drag/drop state.
dragging bool // mouse down on slider
scrollPx int // px from the top where the slider is placed
dragStart render.Point // where the mouse was on click
wasScrollPx int
everyTick func()
}
// NewScrollBar creates a new ScrollBar.
func NewScrollBar(config ScrollBar) *ScrollBar {
w := &ScrollBar{
Frame: NewFrame("Scrollbar Frame"),
Variable: config.Variable,
style: &style.DefaultButton,
Min: config.Min,
Max: config.Max,
Step: config.Step,
}
if w.Max == 0 {
w.Max = 100
}
if w.Step == 0 {
w.Step = 1
}
w.IDFunc(func() string {
return "ScrollBar"
})
w.SetStyle(Theme.Button)
w.setup()
return w
}
// SetStyle sets the ScrollBar style.
func (w *ScrollBar) SetStyle(v *style.Button) {
if v == nil {
v = &style.DefaultButton
}
w.style = v
fmt.Printf("set style: %+v\n", v)
w.Frame.Configure(Config{
BorderSize: w.style.BorderSize,
BorderStyle: BorderSunken,
Background: w.style.Background.Darken(40),
})
}
// GetStyle gets the ScrollBar style.
func (w *ScrollBar) GetStyle() *style.Button {
return w.style
}
// Supervise the ScrollBar. This is necessary for granting mouse-over events
// to the items in the list.
func (w *ScrollBar) Supervise(s *Supervisor) {
w.supervisor = s
// Add all the list items to be supervised.
w.supervisor.Add(w.slider)
for _, c := range w.Frame.Children() {
w.supervisor.Add(c)
}
}
// 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 *ScrollBar) Compute(e render.Engine) {
w.Frame.Compute(e)
if w.everyTick != nil {
w.everyTick()
}
}
// setup the UI components and event handlers.
func (w *ScrollBar) setup() {
w.Configure(Config{
Width: scrollWidth,
})
// The trough that holds the slider.
w.trough = NewFrame("Trough")
// Up button
upBtn := NewButton("Up", NewLabel(Label{
Text: "^",
}))
upBtn.Handle(MouseDown, func(ed EventData) error {
w.everyTick = func() {
w.scrollPx -= w.Step
if w.scrollPx < 0 {
w.scrollPx = 0
}
w.trough.Place(w.slider, Place{
Top: w.scrollPx,
})
w.sendScrollEvent()
}
return nil
})
upBtn.Handle(MouseUp, func(ed EventData) error {
w.everyTick = nil
return nil
})
// The slider
w.slider = NewFrame("Slider")
w.slider.Configure(Config{
BorderSize: w.style.BorderSize,
BorderStyle: BorderStyle(w.style.BorderStyle),
Background: w.style.Background,
Width: scrollWidth - w.BoxThickness(w.style.BorderSize),
Height: scrollbarHeight,
})
// Slider events
w.slider.Handle(MouseOver, func(ed EventData) error {
w.slider.SetBackground(w.style.HoverBackground)
return nil
})
w.slider.Handle(MouseOut, func(ed EventData) error {
w.slider.SetBackground(w.style.Background)
return nil
})
w.slider.Handle(MouseDown, func(ed EventData) error {
w.dragging = true
w.dragStart = ed.Point
w.wasScrollPx = w.scrollPx
fmt.Printf("begin drag from %s\n", ed.Point)
return nil
})
w.slider.Handle(MouseUp, func(ed EventData) error {
fmt.Println("mouse released")
w.dragging = false
return nil