diff --git a/README.md b/README.md index ae2d0e2..6baa1a6 100644 --- a/README.md +++ b/README.md @@ -100,13 +100,15 @@ most complex. **Fully implemented widgets:** +In order of simplicity: + * [x] **BaseWidget**: the base class of all Widgets. * The `Widget` interface describes the functions common to all Widgets, such as SetBackground, Configure, MoveTo, Resize, and so on. * BaseWidget provides sane default implementations for all the methods required by the Widget interface. Most Widgets inherit from - the BaseWidget. -* [x] **Frame**: a layout wrapper for other widgets. + the BaseWidget and override what they need. +* [x] **Frame**: a layout wrapper for child widgets. * Pack() lets you add child widgets to the Frame, aligned against one side or another, and ability to expand widgets to take up remaining space in their part of the Frame. @@ -153,6 +155,10 @@ most complex. a modal pop-up by the MenuButton and MenuBar. [Example](eg/menus) * [x] **SelectBox**: a kind of MenuButton that lets the user choose a value from a list of possible values. +* [x] **Scrollbar**: a Frame including a trough, scroll buttons and a + draggable slider. +* [x] **ListBox**: a multi-line select box with a ScrollBar that can hold arbitrary + child widgets (usually Labels which have a shortcut function for). Some useful helper widgets: @@ -161,16 +167,11 @@ Some useful helper widgets: custom hexadecimal value by hand (needs assistance from your program). [Example](eg/colorpicker) -**Work in progress widgets:** - -* [ ] **Scrollbar**: a Frame including a trough, scroll buttons and a - draggable slider. - -**Wish list for the longer-term future:** +**Planned widgets:** * [ ] **TextBox:** an editable text field that the user can focus and type a value into. - * Would depend on the WindowManager to manage focus for the widgets. +* [ ] **TextArea:** an editable multi-line text field with a scrollbar. ## Supervisor for Interaction diff --git a/eg/README.md b/eg/README.md index d10c3fc..41c6235 100644 --- a/eg/README.md +++ b/eg/README.md @@ -3,17 +3,13 @@ Here are some example programs using go/ui, each accompanied by a screenshot and description: -* [Hello, World!](hello-world/): a basic UI demo with a Label and a - Button. -* [Frame Place()](frame-place/): demonstrates using the Place() layout - management option for Frame widgets. -* [Window Manager](windows/): demonstrates the Window widget and window - management features of the Supervisor. -* [Tooltip](tooltip/): demonstrates the Tooltip widget on a variety of buttons - scattered around the window. +* [Hello, World!](hello-world/): a basic UI demo with a Label and a Button. +* [Frame Placement](frame-place/): demonstrates using the Place() layout management option for Frame widgets. +* [Window Manager](windows/): demonstrates the Window widget and window management features of the Supervisor. +* [Forms](forms/): demonstrates some form controls and the `magicform` helper module for building forms quickly. +* [Tooltip](tooltip/): demonstrates the Tooltip widget on a variety of buttons scattered around the window. * [Menus](menus/): demonstrates various Menu Buttons and a Menu Bar. -* [Themes](themes/): a UI demo that shows off the Default, Flat, and Dark UI - themes as part of experimental theming support. +* [Themes](themes/): a UI demo that shows off the Default, Flat, and Dark UI themes as part of experimental theming support. * [TabFrame](tabframe/): demo for the TabFrame widget showing multiple Windows with tabbed interfaces. * [ColorPicker](colorpicker/): demo for the ColorPicker widget. diff --git a/eg/forms/README.md b/eg/forms/README.md new file mode 100644 index 0000000..d64386c --- /dev/null +++ b/eg/forms/README.md @@ -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) diff --git a/eg/forms/main.go b/eg/forms/main.go index 161f342..63a9591 100644 --- a/eg/forms/main.go +++ b/eg/forms/main.go @@ -6,109 +6,280 @@ import ( "git.kirsle.net/go/render" "git.kirsle.net/go/render/sdl" "git.kirsle.net/go/ui" + "git.kirsle.net/go/ui/magicform" + "git.kirsle.net/go/ui/style" ) func init() { sdl.DefaultFontFilename = "../DejaVuSans.ttf" } +var ( + MenuFont = render.Text{ + Size: 12, + PadX: 4, + PadY: 2, + } + TabFont = render.Text{ + Size: 12, + PadX: 4, + PadY: 2, + } +) + +var ButtonStylePrimary = &style.Button{ + Background: render.RGBA(0, 60, 153, 255), + Foreground: render.White, + HoverBackground: render.RGBA(0, 153, 255, 255), + HoverForeground: render.White, + OutlineColor: render.DarkGrey, + OutlineSize: 1, + BorderStyle: style.BorderRaised, + BorderSize: 2, +} + func main() { - mw, err := ui.NewMainWindow("Forms Test") + mw, err := ui.NewMainWindow("Forms Test", 500, 375) if err != nil { panic(err) } - mw.SetBackground(render.White) + // Tabbed UI. + tabFrame := ui.NewTabFrame("Tabs") + makeAppFrame(mw, tabFrame) + makeAboutFrame(mw, tabFrame) - // Buttons row. - { - frame := ui.NewFrame("Frame 1") - mw.Pack(frame, ui.Pack{ - Side: ui.N, - FillX: true, - Padding: 4, - }) - label := ui.NewLabel(ui.Label{ - Text: "Buttons:", - }) - frame.Pack(label, ui.Pack{ - Side: ui.W, - }) - - // Buttons. - btn := ui.NewButton("Button 1", ui.NewLabel(ui.Label{ - Text: "Click me!", - })) - btn.Handle(ui.Click, func(ed ui.EventData) error { - fmt.Println("Clicked!") - return nil - }) - frame.Pack(btn, ui.Pack{ - Side: ui.W, - PadX: 4, - }) - - mw.Supervisor().Add(btn) - } - - // Selectbox row. - { - frame := ui.NewFrame("Frame 2") - mw.Pack(frame, ui.Pack{ - Side: ui.N, - FillX: true, - Padding: 4, - }) - label := ui.NewLabel(ui.Label{ - Text: "Set window color:", - }) - frame.Pack(label, ui.Pack{ - Side: ui.W, - }) - - var colors = []struct{ - Label string - Value render.Color - }{ - {"White", render.White}, - {"Yellow", render.Yellow}, - {"Cyan", render.Cyan}, - {"Green", render.Green}, - {"Blue", render.RGBA(0, 153, 255, 255)}, - {"Pink", render.Pink}, - } - - // Create the SelectBox and populate its options. - sel := ui.NewSelectBox("Select 1", ui.Label{}) - for _, option := range colors { - sel.AddItem(option.Label, option.Value, func() { - fmt.Printf("Picked option: %s\n", option.Value) - }) - } - - // On change: set the window BG color. - sel.Handle(ui.Change, func(ed ui.EventData) error { - if val, ok := sel.GetValue(); ok { - if color, ok := val.Value.(render.Color); ok { - fmt.Printf("Set background to: %s\n", val.Label) - mw.SetBackground(color) - } else { - fmt.Println("Not a valid color!") - } - } else { - fmt.Println("Not a valid SelectBox value!") - } - return nil - }) - - frame.Pack(sel, ui.Pack{ - Side: ui.W, - PadX: 4, - }) - sel.Supervise(mw.Supervisor()) - mw.Supervisor().Add(sel) // TODO: ideally Supervise() is all that's needed, - // but w/o this extra Add() the Button doesn't react. - } + tabFrame.Supervise(mw.Supervisor()) + mw.Pack(tabFrame, ui.Pack{ + Side: ui.N, + Expand: true, + Padding: 10, + }) + mw.SetBackground(render.Grey) mw.MainLoop() } + +func makeAppFrame(mw *ui.MainWindow, tf *ui.TabFrame) *ui.Frame { + frame := tf.AddTab("Index", ui.NewLabel(ui.Label{ + Text: "Form Controls", + Font: TabFont, + })) + + // Form variables + var ( + bgcolor = render.Grey + letter string + checkBool1 bool + checkBool2 = true + pagerLabel = "Page 1 of 20" + ) + + // Magic Form is a handy module for easily laying out forms of widgets. + form := magicform.Form{ + Supervisor: mw.Supervisor(), + Engine: mw.Engine, + Vertical: true, + LabelWidth: 120, + PadY: 2, + PadX: 8, + } + + // You add to it a list of fields which support all sorts of different + // form control types. + fields := []magicform.Field{ + // Simple text sections - you can write paragraphs or use a bold font + // to make section labels that span the full width of your frame. + { + Label: "Checkbox controls bound to bool values:", + Font: MenuFont, + }, + + // Checkbox widgets: just bind a BoolVariable and this row will draw + // with a checkbox next to a label. + { + Label: "Check this box to toggle a boolean", + Font: MenuFont, + BoolVariable: &checkBool1, + OnClick: func() { + fmt.Printf("The checkbox was clicked! Value is now: %+v\n", checkBool1) + }, + }, + { + Label: "Uncheck this one", + Font: MenuFont, + BoolVariable: &checkBool2, + OnClick: func() { + fmt.Printf("The checkbox was clicked! Value is now: %+v\n", checkBool1) + }, + }, + + // SelectBox widgets: just bind a SelectValue and provide Options and + // it will draw with a label (LabelWidth wide) next to a SelectBox button. + { + Label: "Window color:", + Font: MenuFont, + SelectValue: &bgcolor, + Options: []magicform.Option{ + { + Label: "Grey", + Value: render.Grey, + }, + { + Label: "White", + Value: render.White, + }, + { + Label: "Yellow", + Value: render.Yellow, + }, + { + Label: "Cyan", + Value: render.Cyan, + }, + { + Label: "Green", + Value: render.Green, + }, + { + Label: "Blue", + Value: render.RGBA(0, 153, 255, 255), + }, + { + Label: "Pink", + Value: render.Pink, + }, + }, + OnSelect: func(v interface{}) { + value, _ := v.(render.Color) + mw.SetBackground(value) + }, + }, + + // ListBox widgets + { + Type: magicform.Listbox, + Label: "Favorite letter:", + Font: MenuFont, + SelectValue: &letter, + Options: []magicform.Option{ + {Label: "A is for apple", Value: "A"}, + {Label: "B is for boy", Value: "B"}, + {Label: "C is for cat", Value: "C"}, + {Label: "D is for dog", Value: "D"}, + {Label: "E is for elephant", Value: "E"}, + {Label: "F is for far", Value: "F"}, + {Label: "G is for ghost", Value: "G"}, + {Label: "H is for high", Value: "H"}, + {Label: "I is for inside", Value: "I"}, + {Label: "J is for joker", Value: "J"}, + {Label: "K is for kangaroo", Value: "K"}, + {Label: "L is for lion", Value: "L"}, + {Label: "M is for mouse", Value: "M"}, + {Label: "N is for night", Value: "N"}, + {Label: "O is for over", Value: "O"}, + {Label: "P is for parry", Value: "P"}, + {Label: "Q is for quarry", Value: "Q"}, + {Label: "R is for reality", Value: "R"}, + {Label: "S is for sunshine", Value: "S"}, + {Label: "T is for tree", Value: "T"}, + {Label: "U is for under", Value: "U"}, + {Label: "V is for vehicle", Value: "V"}, + {Label: "W is for watermelon", Value: "W"}, + {Label: "X is for xylophone", Value: "X"}, + {Label: "Y is for yellow", Value: "Y"}, + {Label: "Z is for zebra", Value: "Z"}, + }, + OnSelect: func(v interface{}) { + value, _ := v.(string) + fmt.Printf("You clicked on: %s\n", value) + }, + }, + + // Pager rows to show an easy paginated UI. + // TODO: this is currently broken and Supervisor doesn't pick it up + { + Label: "A paginator when you need one. You can limit MaxPageButtons\n" + + "and the right arrow can keep selecting past the last page.", + }, + { + LabelVariable: &pagerLabel, + Label: "Page:", + Pager: ui.NewPager(ui.Pager{ + Page: 1, + Pages: 20, + PerPage: 10, + MaxPageButtons: 8, + Font: MenuFont, + OnChange: func(page, perPage int) { + fmt.Printf("Pager clicked: page=%d perPage=%d\n", page, perPage) + pagerLabel = fmt.Sprintf("Page %d of %d", page, 20) + }, + }), + }, + + // Simple variable bindings. + { + Type: magicform.Value, + Label: "The first bool var:", + TextVariable: &pagerLabel, + }, + + // Buttons for the bottom of your form. + { + Buttons: []magicform.Field{ + { + Label: "Save", + ButtonStyle: ButtonStylePrimary, + Font: MenuFont, + OnClick: func() { + fmt.Println("Primary button clicked") + }, + }, + { + Label: "Cancel", + Font: MenuFont, + OnClick: func() { + fmt.Println("Secondary button clicked") + }, + }, + }, + }, + } + + form.Create(frame, fields) + return frame +} + +func makeAboutFrame(mw *ui.MainWindow, tf *ui.TabFrame) *ui.Frame { + frame := tf.AddTab("About", ui.NewLabel(ui.Label{ + Text: "About", + Font: TabFont, + })) + + form := magicform.Form{ + Supervisor: mw.Supervisor(), + Engine: mw.Engine, + Vertical: true, + LabelWidth: 120, + PadY: 2, + PadX: 8, + } + + fields := []magicform.Field{ + { + Label: "About", + Font: MenuFont, + }, + + { + Label: "This example shows off the UI toolkit's use for form controls,\n" + + "and how the magicform helper module can make simple forms\n" + + "easy to compose quickly.", + Font: MenuFont, + }, + } + + form.Create(frame, fields) + return frame +} diff --git a/eg/forms/screenshot.png b/eg/forms/screenshot.png new file mode 100644 index 0000000..a30268e Binary files /dev/null and b/eg/forms/screenshot.png differ diff --git a/functions.go b/functions.go index 91f340e..77f7248 100644 --- a/functions.go +++ b/functions.go @@ -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) } } diff --git a/go.mod b/go.mod index 0960981..7f08d07 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.kirsle.net/go/ui go 1.16 require ( - git.kirsle.net/go/render v0.0.0-20211231003948-9e640ab5c3da - github.com/veandco/go-sdl2 v0.4.8 // indirect - golang.org/x/image v0.0.0-20211028202545-6944b10bf410 + git.kirsle.net/go/render v0.0.0-20220505053906-129a24300dfa + github.com/veandco/go-sdl2 v0.4.33 // indirect + golang.org/x/image v0.6.0 ) diff --git a/go.sum b/go.sum index f23c9c7..555fd9c 100644 --- a/go.sum +++ b/go.sum @@ -4,11 +4,18 @@ git.kirsle.net/go/render v0.0.0-20210614025954-d77f5056b782 h1:Ko+NvZxmJbW+M1dA2 git.kirsle.net/go/render v0.0.0-20210614025954-d77f5056b782/go.mod h1:ss7pvZbGWrMaDuZwyUTjV9+T0AJwAkxZZHwMFsvHrkk= git.kirsle.net/go/render v0.0.0-20211231003948-9e640ab5c3da h1:wbeh/hHiwmXqf/3VPrbE/PADTcT1niQWhxxK81Ize3o= git.kirsle.net/go/render v0.0.0-20211231003948-9e640ab5c3da/go.mod h1:ss7pvZbGWrMaDuZwyUTjV9+T0AJwAkxZZHwMFsvHrkk= +git.kirsle.net/go/render v0.0.0-20220505053906-129a24300dfa h1:Oa99SXkmFGnUNy+toPMQyW/eYotN1nZ9BWAThQ/huiM= +git.kirsle.net/go/render v0.0.0-20220505053906-129a24300dfa/go.mod h1:ss7pvZbGWrMaDuZwyUTjV9+T0AJwAkxZZHwMFsvHrkk= github.com/veandco/go-sdl2 v0.4.1/go.mod h1:FB+kTpX9YTE+urhYiClnRzpOXbiWgaU3+5F2AB78DPg= github.com/veandco/go-sdl2 v0.4.7 h1:VfpCM+LfEGDbHdByglCo2bcBsevjFvzl8W0f6VLNitg= github.com/veandco/go-sdl2 v0.4.7/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY= github.com/veandco/go-sdl2 v0.4.8 h1:A26KeX6R1CGt/BQGEov6oxYmVGMMEWDVqTvK1tXvahE= github.com/veandco/go-sdl2 v0.4.8/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY= +github.com/veandco/go-sdl2 v0.4.33 h1:cxQ0OdUBEByHxvCyrGxy9F8WpL38Ya6hzV4n27QL84M= +github.com/veandco/go-sdl2 v0.4.33/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210504121937-7319ad40d33e h1:PzJMNfFQx+QO9hrC1GwZ4BoPGeNGhfeQEgcQFArEjPk= golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -16,6 +23,34 @@ golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jp golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4= +golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/listbox.go b/listbox.go new file mode 100644 index 0000000..472cd4a --- /dev/null +++ b/listbox.go @@ -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) +} diff --git a/magicform/magicform.go b/magicform/magicform.go new file mode 100644 index 0000000..536540b --- /dev/null +++ b/magicform/magicform.go @@ -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 +} diff --git a/scrollbar.go b/scrollbar.go new file mode 100644 index 0000000..8df985c --- /dev/null +++ b/scrollbar.go @@ -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, + }) +} diff --git a/style/button.go b/style/button.go index e29fe72..f567b5d 100644 --- a/style/button.go +++ b/style/button.go @@ -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 +} diff --git a/supervisor.go b/supervisor.go index 51cca1d..cd3fa55 100644 --- a/supervisor.go +++ b/supervisor.go @@ -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) diff --git a/theme/theme.go b/theme/theme.go index 71afe0c..0181d5c 100644 --- a/theme/theme.go +++ b/theme/theme.go @@ -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, }