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:**
|
**Fully implemented widgets:**
|
||||||
|
|
||||||
|
In order of simplicity:
|
||||||
|
|
||||||
* [x] **BaseWidget**: the base class of all Widgets.
|
* [x] **BaseWidget**: the base class of all Widgets.
|
||||||
* The `Widget` interface describes the functions common to all Widgets,
|
* The `Widget` interface describes the functions common to all Widgets,
|
||||||
such as SetBackground, Configure, MoveTo, Resize, and so on.
|
such as SetBackground, Configure, MoveTo, Resize, and so on.
|
||||||
* BaseWidget provides sane default implementations for all the methods
|
* BaseWidget provides sane default implementations for all the methods
|
||||||
required by the Widget interface. Most Widgets inherit from
|
required by the Widget interface. Most Widgets inherit from
|
||||||
the BaseWidget.
|
the BaseWidget and override what they need.
|
||||||
* [x] **Frame**: a layout wrapper for other widgets.
|
* [x] **Frame**: a layout wrapper for child widgets.
|
||||||
* Pack() lets you add child widgets to the Frame, aligned against one side
|
* 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
|
or another, and ability to expand widgets to take up remaining space in
|
||||||
their part of the Frame.
|
their part of the Frame.
|
||||||
|
@ -153,6 +155,10 @@ most complex.
|
||||||
a modal pop-up by the MenuButton and MenuBar. [Example](eg/menus)
|
a modal pop-up by the MenuButton and MenuBar. [Example](eg/menus)
|
||||||
* [x] **SelectBox**: a kind of MenuButton that lets the user choose a
|
* [x] **SelectBox**: a kind of MenuButton that lets the user choose a
|
||||||
value from a list of possible values.
|
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:
|
Some useful helper widgets:
|
||||||
|
|
||||||
|
@ -161,16 +167,11 @@ Some useful helper widgets:
|
||||||
custom hexadecimal value by hand (needs assistance from your program).
|
custom hexadecimal value by hand (needs assistance from your program).
|
||||||
[Example](eg/colorpicker)
|
[Example](eg/colorpicker)
|
||||||
|
|
||||||
**Work in progress widgets:**
|
**Planned widgets:**
|
||||||
|
|
||||||
* [ ] **Scrollbar**: a Frame including a trough, scroll buttons and a
|
|
||||||
draggable slider.
|
|
||||||
|
|
||||||
**Wish list for the longer-term future:**
|
|
||||||
|
|
||||||
* [ ] **TextBox:** an editable text field that the user can focus and type
|
* [ ] **TextBox:** an editable text field that the user can focus and type
|
||||||
a value into.
|
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
|
## 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
|
Here are some example programs using go/ui, each accompanied by a
|
||||||
screenshot and description:
|
screenshot and description:
|
||||||
|
|
||||||
* [Hello, World!](hello-world/): a basic UI demo with a Label and a
|
* [Hello, World!](hello-world/): a basic UI demo with a Label and a Button.
|
||||||
Button.
|
* [Frame Placement](frame-place/): demonstrates using the Place() layout management option for Frame widgets.
|
||||||
* [Frame Place()](frame-place/): demonstrates using the Place() layout
|
* [Window Manager](windows/): demonstrates the Window widget and window management features of the Supervisor.
|
||||||
management option for Frame widgets.
|
* [Forms](forms/): demonstrates some form controls and the `magicform` helper module for building forms quickly.
|
||||||
* [Window Manager](windows/): demonstrates the Window widget and window
|
* [Tooltip](tooltip/): demonstrates the Tooltip widget on a variety of buttons scattered around the window.
|
||||||
management features of the Supervisor.
|
|
||||||
* [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.
|
* [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](themes/): a UI demo that shows off the Default, Flat, and Dark UI themes as part of experimental theming support.
|
||||||
themes as part of experimental theming support.
|
|
||||||
* [TabFrame](tabframe/): demo for the TabFrame widget showing multiple Windows
|
* [TabFrame](tabframe/): demo for the TabFrame widget showing multiple Windows
|
||||||
with tabbed interfaces.
|
with tabbed interfaces.
|
||||||
* [ColorPicker](colorpicker/): demo for the ColorPicker widget.
|
* [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)
|
349
eg/forms/main.go
349
eg/forms/main.go
|
@ -6,109 +6,280 @@ import (
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
"git.kirsle.net/go/render/sdl"
|
"git.kirsle.net/go/render/sdl"
|
||||||
"git.kirsle.net/go/ui"
|
"git.kirsle.net/go/ui"
|
||||||
|
"git.kirsle.net/go/ui/magicform"
|
||||||
|
"git.kirsle.net/go/ui/style"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
sdl.DefaultFontFilename = "../DejaVuSans.ttf"
|
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() {
|
func main() {
|
||||||
mw, err := ui.NewMainWindow("Forms Test")
|
mw, err := ui.NewMainWindow("Forms Test", 500, 375)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mw.SetBackground(render.White)
|
// Tabbed UI.
|
||||||
|
tabFrame := ui.NewTabFrame("Tabs")
|
||||||
|
makeAppFrame(mw, tabFrame)
|
||||||
|
makeAboutFrame(mw, tabFrame)
|
||||||
|
|
||||||
// Buttons row.
|
tabFrame.Supervise(mw.Supervisor())
|
||||||
{
|
mw.Pack(tabFrame, ui.Pack{
|
||||||
frame := ui.NewFrame("Frame 1")
|
|
||||||
mw.Pack(frame, ui.Pack{
|
|
||||||
Side: ui.N,
|
Side: ui.N,
|
||||||
FillX: true,
|
Expand: true,
|
||||||
Padding: 4,
|
Padding: 10,
|
||||||
})
|
|
||||||
label := ui.NewLabel(ui.Label{
|
|
||||||
Text: "Buttons:",
|
|
||||||
})
|
|
||||||
frame.Pack(label, ui.Pack{
|
|
||||||
Side: ui.W,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Buttons.
|
mw.SetBackground(render.Grey)
|
||||||
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()
|
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 (
|
var (
|
||||||
node = w
|
node = w
|
||||||
ok bool
|
ok bool
|
||||||
|
pt render.Point
|
||||||
)
|
)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
@ -20,7 +21,9 @@ func AbsolutePosition(w Widget) render.Point {
|
||||||
return abs
|
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
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.kirsle.net/go/render v0.0.0-20211231003948-9e640ab5c3da
|
git.kirsle.net/go/render v0.0.0-20220505053906-129a24300dfa
|
||||||
github.com/veandco/go-sdl2 v0.4.8 // indirect
|
github.com/veandco/go-sdl2 v0.4.33 // indirect
|
||||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410
|
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-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 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-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.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 h1:VfpCM+LfEGDbHdByglCo2bcBsevjFvzl8W0f6VLNitg=
|
||||||
github.com/veandco/go-sdl2 v0.4.7/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
|
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 h1:A26KeX6R1CGt/BQGEov6oxYmVGMMEWDVqTvK1tXvahE=
|
||||||
github.com/veandco/go-sdl2 v0.4.8/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
|
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-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 h1:PzJMNfFQx+QO9hrC1GwZ4BoPGeNGhfeQEgcQFArEjPk=
|
||||||
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
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-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 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
|
||||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
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.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.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-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
|
||||||
|
})
|
||||||
|
w.slider.Handle(MouseMove, func(ed EventData) error {
|
||||||
|
if w.dragging {
|
||||||
|
var (
|
||||||
|
delta = w.dragStart.Compare(ed.Point)
|
||||||
|
moveTo = w.wasScrollPx + delta.Y
|
||||||
|
)
|
||||||
|
|
||||||
|
if moveTo < 0 {
|
||||||
|
moveTo = 0
|
||||||
|
} else if moveTo > w.trough.height-w.slider.height {
|
||||||
|
moveTo = w.trough.height - w.slider.height
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("delta drag: %s\n", delta)
|
||||||
|
w.scrollPx = moveTo
|
||||||
|
w.trough.Place(w.slider, Place{
|
||||||
|
Top: w.scrollPx,
|
||||||
|
})
|
||||||
|
w.sendScrollEvent()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
downBtn := NewButton("Down", NewLabel(Label{
|
||||||
|
Text: "v",
|
||||||
|
}))
|
||||||
|
downBtn.Handle(MouseDown, func(ed EventData) error {
|
||||||
|
w.everyTick = func() {
|
||||||
|
w.scrollPx += w.Step
|
||||||
|
if w.scrollPx > w.trough.height-w.slider.height {
|
||||||
|
w.scrollPx = w.trough.height - w.slider.height
|
||||||
|
}
|
||||||
|
w.trough.Place(w.slider, Place{
|
||||||
|
Top: w.scrollPx,
|
||||||
|
})
|
||||||
|
w.sendScrollEvent()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
downBtn.Handle(MouseUp, func(ed EventData) error {
|
||||||
|
w.everyTick = nil
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
w.Frame.Pack(upBtn, Pack{
|
||||||
|
Side: N,
|
||||||
|
FillX: true,
|
||||||
|
})
|
||||||
|
w.Frame.Pack(w.trough, Pack{
|
||||||
|
Side: N,
|
||||||
|
Fill: true,
|
||||||
|
Expand: true,
|
||||||
|
})
|
||||||
|
w.trough.Place(w.slider, Place{
|
||||||
|
Top: w.scrollPx,
|
||||||
|
Left: 0,
|
||||||
|
})
|
||||||
|
w.Frame.Pack(downBtn, Pack{
|
||||||
|
Side: N,
|
||||||
|
FillX: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present the scrollbar.
|
||||||
|
func (w *ScrollBar) Present(e render.Engine, p render.Point) {
|
||||||
|
w.Frame.Present(e, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ScrollBar) sendScrollEvent() {
|
||||||
|
var fraction float64
|
||||||
|
if w.scrollPx > 0 {
|
||||||
|
fraction = float64(w.scrollPx) / (float64(w.trough.height) - float64(w.slider.height))
|
||||||
|
}
|
||||||
|
w.Event(Scroll, EventData{
|
||||||
|
ScrollFraction: fraction,
|
||||||
|
ScrollUnits: int(fraction * float64(w.Max)),
|
||||||
|
ScrollPages: 0,
|
||||||
|
})
|
||||||
|
}
|
|
@ -30,6 +30,18 @@ var (
|
||||||
BorderSize: 2,
|
BorderSize: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DefaultListBox = ListBox{
|
||||||
|
Background: render.White,
|
||||||
|
Foreground: render.Black,
|
||||||
|
HoverBackground: render.Cyan,
|
||||||
|
HoverForeground: render.Orange,
|
||||||
|
SelectedBackground: render.Blue,
|
||||||
|
SelectedForeground: render.White,
|
||||||
|
BorderStyle: BorderSunken,
|
||||||
|
// BorderColor: render.RGBA(200, 200, 200, 255),
|
||||||
|
BorderSize: 2,
|
||||||
|
}
|
||||||
|
|
||||||
DefaultTooltip = Tooltip{
|
DefaultTooltip = Tooltip{
|
||||||
Background: render.RGBA(0, 0, 0, 230),
|
Background: render.RGBA(0, 0, 0, 230),
|
||||||
Foreground: render.White,
|
Foreground: render.White,
|
||||||
|
@ -69,3 +81,15 @@ type Tooltip struct {
|
||||||
Background render.Color
|
Background render.Color
|
||||||
Foreground render.Color
|
Foreground render.Color
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListBox style configuration.
|
||||||
|
type ListBox struct {
|
||||||
|
Background render.Color
|
||||||
|
Foreground render.Color // Labels only
|
||||||
|
SelectedBackground render.Color
|
||||||
|
SelectedForeground render.Color
|
||||||
|
HoverBackground render.Color
|
||||||
|
HoverForeground render.Color
|
||||||
|
BorderStyle BorderStyle
|
||||||
|
BorderSize int
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ const (
|
||||||
KeyDown
|
KeyDown
|
||||||
KeyUp
|
KeyUp
|
||||||
KeyPress
|
KeyPress
|
||||||
|
Scroll
|
||||||
|
|
||||||
// Drag/drop event handlers.
|
// Drag/drop event handlers.
|
||||||
DragStop // if a widget is being dragged and the drag is done
|
DragStop // if a widget is being dragged and the drag is done
|
||||||
|
@ -60,6 +61,23 @@ type EventData struct {
|
||||||
// Clicked is true if the primary mouse button is down during
|
// Clicked is true if the primary mouse button is down during
|
||||||
// a MouseMove
|
// a MouseMove
|
||||||
Clicked bool
|
Clicked bool
|
||||||
|
|
||||||
|
// A Value given e.g. from a ListBox click.
|
||||||
|
Value interface{}
|
||||||
|
|
||||||
|
// Scroll event values.
|
||||||
|
ScrollFraction float64 // between 0 and 1 for the scrollbar percentage
|
||||||
|
|
||||||
|
// Number of units that have scrolled. It is up to the caller to decide
|
||||||
|
// what units mean (e.g. characters, lines of text, pixels, etc.)
|
||||||
|
// The scrollbar fraction times your Step value provides the units.
|
||||||
|
ScrollUnits int
|
||||||
|
|
||||||
|
// Number of pages that have scrolled. It is up to the caller to decide
|
||||||
|
// what a page is. It would typically be a number of your Units slightly
|
||||||
|
// less than what fits in the list so the user sees some overlap as
|
||||||
|
// they scroll quickly by pages.
|
||||||
|
ScrollPages int // TODO: not implemented
|
||||||
}
|
}
|
||||||
|
|
||||||
// RelativePoint returns the ed.Point adjusted to be relative to the widget on screen.
|
// RelativePoint returns the ed.Point adjusted to be relative to the widget on screen.
|
||||||
|
@ -262,11 +280,13 @@ func (s *Supervisor) Hovering(cursor render.Point) (hovering, outside []WidgetSl
|
||||||
// window, so that these widgets always get events.
|
// window, so that these widgets always get events.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
|
//
|
||||||
// XY (Point): mouse cursor position as calculated in Loop()
|
// XY (Point): mouse cursor position as calculated in Loop()
|
||||||
// ev, hovering, outside: values from Loop(), self explanatory.
|
// ev, hovering, outside: values from Loop(), self explanatory.
|
||||||
// behavior: indicates how this method is being used.
|
// behavior: indicates how this method is being used.
|
||||||
//
|
//
|
||||||
// behavior options:
|
// behavior options:
|
||||||
|
//
|
||||||
// 0: widgets NOT part of a managed window. On this pass, if a widget IS
|
// 0: widgets NOT part of a managed window. On this pass, if a widget IS
|
||||||
// a part of a window, it gets no events triggered.
|
// a part of a window, it gets no events triggered.
|
||||||
// 1: widgets are part of the active focused window.
|
// 1: widgets are part of the active focused window.
|
||||||
|
|
|
@ -21,6 +21,7 @@ type Theme struct {
|
||||||
Window *style.Window
|
Window *style.Window
|
||||||
Label *style.Label
|
Label *style.Label
|
||||||
Button *style.Button
|
Button *style.Button
|
||||||
|
ListBox *style.ListBox
|
||||||
Tooltip *style.Tooltip
|
Tooltip *style.Tooltip
|
||||||
TabFrame *style.Button
|
TabFrame *style.Button
|
||||||
}
|
}
|
||||||
|
@ -30,6 +31,7 @@ var Default = Theme{
|
||||||
Name: "Default",
|
Name: "Default",
|
||||||
Label: &style.DefaultLabel,
|
Label: &style.DefaultLabel,
|
||||||
Button: &style.DefaultButton,
|
Button: &style.DefaultButton,
|
||||||
|
ListBox: &style.DefaultListBox,
|
||||||
Tooltip: &style.DefaultTooltip,
|
Tooltip: &style.DefaultTooltip,
|
||||||
TabFrame: &style.DefaultButton,
|
TabFrame: &style.DefaultButton,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user