ListBox, ScrollBar, Magic Form and forms demo

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

View File

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

View File

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

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

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

View File

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

BIN
eg/forms/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

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

6
go.mod
View File

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

35
go.sum
View File

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

309
listbox.go Normal file
View File

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

503
magicform/magicform.go Normal file
View File

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

254
scrollbar.go Normal file
View File

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

View File

@ -30,6 +30,18 @@ var (
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{
Background: render.RGBA(0, 0, 0, 230),
Foreground: render.White,
@ -69,3 +81,15 @@ type Tooltip struct {
Background 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
}

View File

@ -23,6 +23,7 @@ const (
KeyDown
KeyUp
KeyPress
Scroll
// Drag/drop event handlers.
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
// a MouseMove
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.
@ -256,20 +274,22 @@ func (s *Supervisor) Hovering(cursor render.Point) (hovering, outside []WidgetSl
// cursor, transmit mouse events to the widgets.
//
// This function has two use cases:
// - In runWindowEvents where we run events for the top-most focused window of
// the window manager.
// - In Supervisor.Loop() for the widgets that are NOT owned by a managed
// window, so that these widgets always get events.
// - In runWindowEvents where we run events for the top-most focused window of
// the window manager.
// - In Supervisor.Loop() for the widgets that are NOT owned by a managed
// window, so that these widgets always get events.
//
// Parameters:
// XY (Point): mouse cursor position as calculated in Loop()
// ev, hovering, outside: values from Loop(), self explanatory.
// behavior: indicates how this method is being used.
//
// XY (Point): mouse cursor position as calculated in Loop()
// ev, hovering, outside: values from Loop(), self explanatory.
// behavior: indicates how this method is being used.
//
// behavior options:
// 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.
// 1: widgets are part of the active focused window.
//
// 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.
// 1: widgets are part of the active focused window.
func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State,
hovering, outside []WidgetSlot, toFocusedWindow bool) (bool, error) {
// Do we run any events?
@ -587,11 +607,11 @@ UI which you had called Present() on prior.
The current draw order of the Supervisor is as follows:
1. Managed windows are drawn in the order of most recently focused on top.
2. Pop-up modals such as Menus are drawn. Modals have an "event grab" and all
mouse events go to them, or clicking outside of them dismisses the modals.
3. DrawOnTop widgets such as Tooltips that should always be drawn "last" so as
not to be overwritten by neighboring widgets.
1. Managed windows are drawn in the order of most recently focused on top.
2. Pop-up modals such as Menus are drawn. Modals have an "event grab" and all
mouse events go to them, or clicking outside of them dismisses the modals.
3. DrawOnTop widgets such as Tooltips that should always be drawn "last" so as
not to be overwritten by neighboring widgets.
*/
func (s *Supervisor) DrawOnTop(w Widget) {
s.onTop = append(s.onTop, w)

View File

@ -21,6 +21,7 @@ type Theme struct {
Window *style.Window
Label *style.Label
Button *style.Button
ListBox *style.ListBox
Tooltip *style.Tooltip
TabFrame *style.Button
}
@ -30,6 +31,7 @@ var Default = Theme{
Name: "Default",
Label: &style.DefaultLabel,
Button: &style.DefaultButton,
ListBox: &style.DefaultListBox,
Tooltip: &style.DefaultTooltip,
TabFrame: &style.DefaultButton,
}