ListBox, ScrollBar, Magic Form and forms demo
This commit is contained in:
parent
d82ef0b751
commit
8716c479e9
19
README.md
19
README.md
|
@ -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
|
||||
|
||||
|
|
16
eg/README.md
16
eg/README.md
|
@ -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
9
eg/forms/README.md
Normal 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)
|
353
eg/forms/main.go
353
eg/forms/main.go
|
@ -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
BIN
eg/forms/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
|
@ -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
6
go.mod
|
@ -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
35
go.sum
|
@ -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
309
listbox.go
Normal 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
503
magicform/magicform.go
Normal 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
254
scrollbar.go
Normal 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
|
||||