Compare commits
34 Commits
Author | SHA1 | Date |
---|---|---|
Noah | 20a9d7bdff | |
Noah | e912e2bd03 | |
Noah | 28280f08bd | |
Noah | 14bd7446ef | |
Noah | 98dfa2cce5 | |
Noah | 8716c479e9 | |
dependabot[bot] | 43b95a9232 | |
Noah | d82ef0b751 | |
Noah | c99e79d9b0 | |
Noah | 3b653e503c | |
Noah | c9c7b33647 | |
Noah | 76ddda352d | |
Noah | 49b5cfd037 | |
Noah | fee6e1e105 | |
Noah | 0a6054baa6 | |
Noah | 79210ae8c9 | |
Noah | 49992aad2a | |
Noah | e8d4e7008b | |
Noah | f6703bf1ba | |
Noah | 5d16f5d50c | |
Noah | e7e8b4b2c1 | |
Noah | b87b4825af | |
Noah | 9a25ec3782 | |
Noah | 6df7bade48 | |
Noah | 8f91971b62 | |
Noah Petherbridge | ff76b831ad | |
Noah | b7190fe958 | |
Noah | c32601391a | |
Noah | 8433b1b216 | |
Noah | e2a561fbd0 | |
Noah | 0e027a9fee | |
Noah | e675ead0ed | |
Noah | 4206330398 | |
Noah | 53c0fed7be |
48
README.md
|
@ -8,25 +8,28 @@ applications (SDL2, for Linux, MacOS and Windows) as well as web browsers
|
||||||
|
|
||||||
![Screenshot](docs/guitest.png)
|
![Screenshot](docs/guitest.png)
|
||||||
|
|
||||||
> _(Screenshot is from Project: Doodle's GUITest debug screen showing a_
|
> _(Screenshot is from Sketchy Maze's GUITest debug screen showing a_
|
||||||
> _Window, several Frames, Labels, Buttons and a Checkbox widget.)_
|
> _Window, several Frames, Labels, Buttons and a Checkbox widget.)_
|
||||||
|
|
||||||
It is very much a **work in progress** and may contain bugs and its API may
|
It is very much a **work in progress** and may contain bugs and its API may
|
||||||
change as bugs are fixed or features added.
|
change as bugs are fixed or features added.
|
||||||
|
|
||||||
This library is being developed in conjunction with my drawing-based maze
|
This library is being developed in conjunction with my drawing-based maze
|
||||||
game, [Project: Doodle](https://www.kirsle.net/doodle). The rendering engine
|
game, [Sketchy Maze](https://www.sketchymaze.com). The rendering engine
|
||||||
library is at [go/render](https://git.kirsle.net/go/render) which provides
|
library is at [go/render](https://git.kirsle.net/go/render) which provides
|
||||||
the SDL2 and Canvas back-ends.
|
the SDL2 and Canvas back-ends.
|
||||||
(GitHub mirror: [kirsle/render](https://github.com/kirsle/render))
|
(GitHub mirror: [kirsle/render](https://github.com/kirsle/render))
|
||||||
|
|
||||||
**Notice:** the canonical source repository for this project is at
|
**Notice:** the canonical source repository for this project is at
|
||||||
[git.kirsle.net/go/ui](https://git.kirsle.net/go/ui) with a mirror available
|
[git.kirsle.net/go/ui](https://git.kirsle.net/go/ui) with a mirror available
|
||||||
on GitHub at [kirsle/ui](https://github.com/kirsle/ui). Issues and pull
|
on GitHub at [SketchyMaze/ui](https://github.com/SketchyMaze/ui). Issues and pull
|
||||||
requests are accepted on GitHub.
|
requests are accepted on GitHub.
|
||||||
|
|
||||||
# Example
|
# Example
|
||||||
|
|
||||||
|
See the [eg/](https://git.kirsle.net/go/ui/src/branch/master/eg) directory
|
||||||
|
in this git repository for several example programs and screenshots.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
@ -97,19 +100,22 @@ most complex.
|
||||||
|
|
||||||
**Fully implemented widgets:**
|
**Fully implemented widgets:**
|
||||||
|
|
||||||
|
In order of simplicity:
|
||||||
|
|
||||||
* [x] **BaseWidget**: the base class of all Widgets.
|
* [x] **BaseWidget**: the base class of all Widgets.
|
||||||
* The `Widget` interface describes the functions common to all Widgets,
|
* The `Widget` interface describes the functions common to all Widgets,
|
||||||
such as SetBackground, Configure, MoveTo, Resize, and so on.
|
such as SetBackground, Configure, MoveTo, Resize, and so on.
|
||||||
* BaseWidget provides sane default implementations for all the methods
|
* BaseWidget provides sane default implementations for all the methods
|
||||||
required by the Widget interface. Most Widgets inherit from
|
required by the Widget interface. Most Widgets inherit from
|
||||||
the BaseWidget.
|
the BaseWidget and override what they need.
|
||||||
* [x] **Frame**: a layout wrapper for other widgets.
|
* [x] **Frame**: a layout wrapper for child widgets.
|
||||||
* Pack() lets you add child widgets to the Frame, aligned against one side
|
* Pack() lets you add child widgets to the Frame, aligned against one side
|
||||||
or another, and ability to expand widgets to take up remaining space in
|
or another, and ability to expand widgets to take up remaining space in
|
||||||
their part of the Frame.
|
their part of the Frame.
|
||||||
* Place() lets you place child widgets relative to the parent. You can place
|
* Place() lets you place child widgets relative to the parent. You can place
|
||||||
it at an exact Point, or against the Top, Left, Bottom or Right sides, or
|
it at an exact Point, or against the Top, Left, Bottom or Right sides, or
|
||||||
aligned to the Center (horizontal) or Middle (vertical) of the parent.
|
aligned to the Center (horizontal) or Middle (vertical) of the parent.
|
||||||
|
[Example](eg/frame-place)
|
||||||
* [x] **Label**: Textual labels for your UI.
|
* [x] **Label**: Textual labels for your UI.
|
||||||
* Supports TrueType fonts, color, stroke, drop shadow, font size, etc.
|
* Supports TrueType fonts, color, stroke, drop shadow, font size, etc.
|
||||||
* Variable binding support: TextVariable or IntVariable can point to a
|
* Variable binding support: TextVariable or IntVariable can point to a
|
||||||
|
@ -133,29 +139,39 @@ most complex.
|
||||||
* Works the same as CheckButton and RadioButton but draws a separate
|
* Works the same as CheckButton and RadioButton but draws a separate
|
||||||
label next to a small check button. Clicking the label will toggle the
|
label next to a small check button. Clicking the label will toggle the
|
||||||
state of the checkbox.
|
state of the checkbox.
|
||||||
|
* [x] **TabFrame:** a collection of Frames navigated between using a row
|
||||||
|
of tab buttons along their top edge. [Example](eg/tabframe).
|
||||||
|
* [x] **Pager**: a series of numbered buttons to use with a paginated UI.
|
||||||
|
Includes "Forward" and "Next" buttons and buttons for each page number.
|
||||||
* [x] **Window**: a Frame with a title bar Frame on top.
|
* [x] **Window**: a Frame with a title bar Frame on top.
|
||||||
* Can be managed by Supervisor to give Window Manager controls to it
|
* Can be managed by Supervisor to give Window Manager controls to it
|
||||||
(drag it by its title bar, Close button, window focus, multiple overlapping
|
(drag it by its title bar, Close button, window focus, multiple overlapping
|
||||||
windows, and so on).
|
windows, and so on). [Example](eg/windows)
|
||||||
* [x] **Tooltip**: a mouse hover label attached to a widget.
|
* [x] **Tooltip**: a mouse hover label attached to a widget. [Example](eg/tooltip)
|
||||||
* [x] **MenuButton**: a button that opens a modal pop-up menu on click.
|
* [x] **MenuButton**: a button that opens a modal pop-up menu on click.
|
||||||
* [x] **MenuBar**: a specialized Frame that groups a bunch of MenuButtons and
|
* [x] **MenuBar**: a specialized Frame that groups a bunch of MenuButtons and
|
||||||
provides a simple API to add menus and items to it.
|
provides a simple API to add menus and items to it.
|
||||||
* [x] **Menu**: a frame full of clickable links and separators. Usually used as
|
* [x] **Menu**: a frame full of clickable links and separators. Usually used as
|
||||||
a modal pop-up by the MenuButton and MenuBar.
|
a modal pop-up by the MenuButton and MenuBar. [Example](eg/menus)
|
||||||
|
* [x] **SelectBox**: a kind of MenuButton that lets the user choose a
|
||||||
**Work in progress widgets:**
|
value from a list of possible values.
|
||||||
|
* [x] **Scrollbar**: a Frame including a trough, scroll buttons and a
|
||||||
* [ ] **Scrollbar**: a Frame including a trough, scroll buttons and a
|
|
||||||
draggable slider.
|
draggable slider.
|
||||||
* [ ] **SelectBox:** a kind of MenuButton that lets the user choose a value
|
* [x] **ListBox**: a multi-line select box with a ScrollBar that can hold arbitrary
|
||||||
from a list of possible values, bound to a string variable.
|
child widgets (usually Labels which have a shortcut function for).
|
||||||
|
|
||||||
**Wish list for the longer-term future:**
|
Some useful helper widgets:
|
||||||
|
|
||||||
|
* **ColorPicker**: a ui.Window popup that lets the user choose a color value.
|
||||||
|
It shows a graphical gradient they can click on and an ability to enter a
|
||||||
|
custom hexadecimal value by hand (needs assistance from your program).
|
||||||
|
[Example](eg/colorpicker)
|
||||||
|
|
||||||
|
**Planned widgets:**
|
||||||
|
|
||||||
* [ ] **TextBox:** an editable text field that the user can focus and type
|
* [ ] **TextBox:** an editable text field that the user can focus and type
|
||||||
a value into.
|
a value into.
|
||||||
* Would depend on the WindowManager to manage focus for the widgets.
|
* [ ] **TextArea:** an editable multi-line text field with a scrollbar.
|
||||||
|
|
||||||
## Supervisor for Interaction
|
## Supervisor for Interaction
|
||||||
|
|
||||||
|
|
62
button.go
|
@ -5,13 +5,19 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
"git.kirsle.net/go/ui/theme"
|
"git.kirsle.net/go/ui/style"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Button is a clickable button.
|
// Button is a clickable button.
|
||||||
type Button struct {
|
type Button struct {
|
||||||
BaseWidget
|
BaseWidget
|
||||||
|
Name string
|
||||||
child Widget
|
child Widget
|
||||||
|
style *style.Button
|
||||||
|
|
||||||
|
// Set this true to hard-set a color for this button;
|
||||||
|
// it will not adjust on mouse-over or press.
|
||||||
|
FixedColor bool
|
||||||
|
|
||||||
// Private options.
|
// Private options.
|
||||||
hovering bool
|
hovering bool
|
||||||
|
@ -21,28 +27,34 @@ type Button struct {
|
||||||
// NewButton creates a new Button.
|
// NewButton creates a new Button.
|
||||||
func NewButton(name string, child Widget) *Button {
|
func NewButton(name string, child Widget) *Button {
|
||||||
w := &Button{
|
w := &Button{
|
||||||
|
Name: name,
|
||||||
child: child,
|
child: child,
|
||||||
|
style: &style.DefaultButton,
|
||||||
}
|
}
|
||||||
w.IDFunc(func() string {
|
w.IDFunc(func() string {
|
||||||
return fmt.Sprintf("Button<%s>", name)
|
return fmt.Sprintf("Button<%s>", w.Name)
|
||||||
})
|
})
|
||||||
|
|
||||||
w.Configure(Config{
|
w.SetStyle(Theme.Button)
|
||||||
BorderSize: 2,
|
|
||||||
BorderStyle: BorderRaised,
|
|
||||||
OutlineSize: 1,
|
|
||||||
OutlineColor: theme.ButtonOutlineColor,
|
|
||||||
Background: theme.ButtonBackgroundColor,
|
|
||||||
})
|
|
||||||
|
|
||||||
w.Handle(MouseOver, func(e EventData) error {
|
w.Handle(MouseOver, func(e EventData) error {
|
||||||
w.hovering = true
|
w.hovering = true
|
||||||
w.SetBackground(theme.ButtonHoverColor)
|
if !w.FixedColor {
|
||||||
|
w.SetBackground(w.style.HoverBackground)
|
||||||
|
if label, ok := w.child.(*Label); ok {
|
||||||
|
label.Font.Color = w.style.HoverForeground
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
w.Handle(MouseOut, func(e EventData) error {
|
w.Handle(MouseOut, func(e EventData) error {
|
||||||
w.hovering = false
|
w.hovering = false
|
||||||
w.SetBackground(theme.ButtonBackgroundColor)
|
if !w.FixedColor {
|
||||||
|
w.SetBackground(w.style.Background)
|
||||||
|
if label, ok := w.child.(*Label); ok {
|
||||||
|
label.Font.Color = w.style.Foreground
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -53,13 +65,39 @@ func NewButton(name string, child Widget) *Button {
|
||||||
})
|
})
|
||||||
w.Handle(MouseUp, func(e EventData) error {
|
w.Handle(MouseUp, func(e EventData) error {
|
||||||
w.clicked = false
|
w.clicked = false
|
||||||
w.SetBorderStyle(BorderRaised)
|
w.SetBorderStyle(BorderStyle(w.style.BorderStyle))
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetStyle sets the button style.
|
||||||
|
func (w *Button) SetStyle(v *style.Button) {
|
||||||
|
if v == nil {
|
||||||
|
v = &style.DefaultButton
|
||||||
|
}
|
||||||
|
|
||||||
|
w.style = v
|
||||||
|
w.Configure(Config{
|
||||||
|
BorderSize: w.style.BorderSize,
|
||||||
|
BorderStyle: BorderStyle(w.style.BorderStyle),
|
||||||
|
OutlineSize: w.style.OutlineSize,
|
||||||
|
OutlineColor: w.style.OutlineColor,
|
||||||
|
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 button style.
|
||||||
|
func (w *Button) GetStyle() *style.Button {
|
||||||
|
return w.style
|
||||||
|
}
|
||||||
|
|
||||||
// Children returns the button's child widget.
|
// Children returns the button's child widget.
|
||||||
func (w *Button) Children() []Widget {
|
func (w *Button) Children() []Widget {
|
||||||
return []Widget{w.child}
|
return []Widget{w.child}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
"git.kirsle.net/go/ui/theme"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CheckButton implements a checkbox and radiobox widget. It's based on a
|
// CheckButton implements a checkbox and radiobox widget. It's based on a
|
||||||
|
@ -28,6 +27,8 @@ func NewCheckButton(name string, boolVar *bool, child Widget) *CheckButton {
|
||||||
return fmt.Sprintf("CheckButton<%s %+v>", name, w.BoolVar)
|
return fmt.Sprintf("CheckButton<%s %+v>", name, w.BoolVar)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
w.SetStyle(Theme.Button)
|
||||||
|
|
||||||
w.setup()
|
w.setup()
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
@ -42,6 +43,9 @@ func NewRadioButton(name string, stringVar *string, value string, child Widget)
|
||||||
w.IDFunc(func() string {
|
w.IDFunc(func() string {
|
||||||
return fmt.Sprintf(`RadioButton<%s "%s" %s>`, name, w.Value, strconv.FormatBool(*w.StringVar == w.Value))
|
return fmt.Sprintf(`RadioButton<%s "%s" %s>`, name, w.Value, strconv.FormatBool(*w.StringVar == w.Value))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
w.SetStyle(Theme.Button)
|
||||||
|
|
||||||
w.setup()
|
w.setup()
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
@ -57,16 +61,28 @@ func (w *CheckButton) Compute(e render.Engine) {
|
||||||
} else {
|
} else {
|
||||||
w.SetBorderStyle(BorderRaised)
|
w.SetBorderStyle(BorderRaised)
|
||||||
}
|
}
|
||||||
|
} else if w.BoolVar != nil {
|
||||||
|
// Checkbutton, always re-assign the border style in case the caller
|
||||||
|
// has flipped the boolean behind our back.
|
||||||
|
if *w.BoolVar {
|
||||||
|
w.SetBorderStyle(BorderSunken)
|
||||||
|
} else {
|
||||||
|
w.SetBorderStyle(BorderRaised)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
w.Button.Compute(e)
|
w.Button.Compute(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup the common things between checkboxes and radioboxes.
|
// setup the common things between checkboxes and radioboxes.
|
||||||
func (w *CheckButton) setup() {
|
func (w *CheckButton) setup() {
|
||||||
var borderStyle BorderStyle = BorderRaised
|
var (
|
||||||
|
borderStyle BorderStyle = BorderRaised
|
||||||
|
background = w.style.Background
|
||||||
|
)
|
||||||
if w.BoolVar != nil {
|
if w.BoolVar != nil {
|
||||||
if *w.BoolVar == true {
|
if *w.BoolVar == true {
|
||||||
borderStyle = BorderSunken
|
borderStyle = BorderSunken
|
||||||
|
background = w.style.Background.Darken(40)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,18 +90,31 @@ func (w *CheckButton) setup() {
|
||||||
BorderSize: 2,
|
BorderSize: 2,
|
||||||
BorderStyle: borderStyle,
|
BorderStyle: borderStyle,
|
||||||
OutlineSize: 1,
|
OutlineSize: 1,
|
||||||
OutlineColor: theme.ButtonOutlineColor,
|
OutlineColor: w.style.OutlineColor,
|
||||||
Background: theme.ButtonBackgroundColor,
|
Background: background,
|
||||||
})
|
})
|
||||||
|
|
||||||
w.Handle(MouseOver, func(ed EventData) error {
|
w.Handle(MouseOver, func(ed EventData) error {
|
||||||
w.hovering = true
|
w.hovering = true
|
||||||
w.SetBackground(theme.ButtonHoverColor)
|
w.SetBackground(w.style.HoverBackground)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
w.Handle(MouseOut, func(ed EventData) error {
|
w.Handle(MouseOut, func(ed EventData) error {
|
||||||
w.hovering = false
|
w.hovering = false
|
||||||
w.SetBackground(theme.ButtonBackgroundColor)
|
|
||||||
|
var sunken bool
|
||||||
|
if w.BoolVar != nil {
|
||||||
|
sunken = *w.BoolVar == true
|
||||||
|
} else if w.StringVar != nil {
|
||||||
|
sunken = *w.StringVar == w.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
if sunken {
|
||||||
|
w.SetBackground(w.style.Background.Darken(40))
|
||||||
|
} else {
|
||||||
|
w.SetBackground(w.style.Background)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -115,9 +144,12 @@ func (w *CheckButton) setup() {
|
||||||
|
|
||||||
if sunken {
|
if sunken {
|
||||||
w.SetBorderStyle(BorderSunken)
|
w.SetBorderStyle(BorderSunken)
|
||||||
|
w.SetBackground(w.style.Background.Darken(40))
|
||||||
} else {
|
} else {
|
||||||
w.SetBorderStyle(BorderRaised)
|
w.SetBorderStyle(BorderRaised)
|
||||||
|
w.SetBackground(w.style.Background)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
21
checkbox.go
|
@ -1,5 +1,7 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
// Checkbox combines a CheckButton with a widget like a Label.
|
// Checkbox combines a CheckButton with a widget like a Label.
|
||||||
type Checkbox struct {
|
type Checkbox struct {
|
||||||
Frame
|
Frame
|
||||||
|
@ -21,6 +23,10 @@ func NewRadiobox(name string, stringVar *string, value string, child Widget) *Ch
|
||||||
func makeCheckbox(name string, boolVar *bool, stringVar *string, value string, child Widget) *Checkbox {
|
func makeCheckbox(name string, boolVar *bool, stringVar *string, value string, child Widget) *Checkbox {
|
||||||
// Our custom checkbutton widget.
|
// Our custom checkbutton widget.
|
||||||
mark := NewFrame(name + "_mark")
|
mark := NewFrame(name + "_mark")
|
||||||
|
mark.Configure(Config{
|
||||||
|
Width: 6,
|
||||||
|
Height: 6,
|
||||||
|
})
|
||||||
|
|
||||||
w := &Checkbox{
|
w := &Checkbox{
|
||||||
child: child,
|
child: child,
|
||||||
|
@ -33,7 +39,7 @@ func makeCheckbox(name string, boolVar *bool, stringVar *string, value string, c
|
||||||
w.Frame.Setup()
|
w.Frame.Setup()
|
||||||
|
|
||||||
// Forward clicks on the child widget to the CheckButton.
|
// Forward clicks on the child widget to the CheckButton.
|
||||||
for _, e := range []Event{MouseOver, MouseOut, MouseUp, MouseDown} {
|
for _, e := range []Event{MouseOver, MouseOut, MouseUp, MouseDown, Click} {
|
||||||
func(e Event) {
|
func(e Event) {
|
||||||
w.child.Handle(e, func(ed EventData) error {
|
w.child.Handle(e, func(ed EventData) error {
|
||||||
return w.button.Event(e, ed)
|
return w.button.Event(e, ed)
|
||||||
|
@ -56,6 +62,19 @@ func (w *Checkbox) Child() Widget {
|
||||||
return w.child
|
return w.child
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetText conveniently sets the button text, for Label children only.
|
||||||
|
func (w *Checkbox) SetText(text string) error {
|
||||||
|
if label, ok := w.child.(*Label); ok {
|
||||||
|
label.Text = text
|
||||||
|
}
|
||||||
|
return errors.New("child is not a Label widget")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass event handlers on to descendents.
|
||||||
|
func (w *Checkbox) Handle(e Event, fn func(EventData) error) {
|
||||||
|
w.button.Handle(e, fn)
|
||||||
|
}
|
||||||
|
|
||||||
// Supervise the checkbutton inside the widget.
|
// Supervise the checkbutton inside the widget.
|
||||||
func (w *Checkbox) Supervise(s *Supervisor) {
|
func (w *Checkbox) Supervise(s *Supervisor) {
|
||||||
s.Add(w.button)
|
s.Add(w.button)
|
||||||
|
|
|
@ -0,0 +1,584 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
|
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ColorPicker is a Window that allows the user to pick out a color.
|
||||||
|
type ColorPicker struct {
|
||||||
|
*Window
|
||||||
|
|
||||||
|
// Config settings.
|
||||||
|
Title string
|
||||||
|
Color render.Color // initial color selection
|
||||||
|
Supervisor *Supervisor
|
||||||
|
Engine render.Engine
|
||||||
|
|
||||||
|
// Callback function in case the user wants to manually enter a hex color code.
|
||||||
|
// Your program should prompt them by any means and return their chosen color.
|
||||||
|
// Return a zero color (render.Invisible) to mean cancel.
|
||||||
|
OnManualInput func(callback func(render.Color))
|
||||||
|
|
||||||
|
then func(render.Color) // .Then() callback
|
||||||
|
cancel func() // .OnCancel() callback
|
||||||
|
selected render.Color
|
||||||
|
|
||||||
|
// SDL2 etc. structures that will need freed.
|
||||||
|
tex render.Texturer
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultColorPickerSize sets the default window size used in NewColorPicker
|
||||||
|
// unless the user specified overrides.
|
||||||
|
var DefaultColorPickerSize = render.Rect{W: 240, H: 190}
|
||||||
|
|
||||||
|
// NewColorPicker creates a new ColorPicker window. Specify the dimensions
|
||||||
|
// you want the window to appear in (width, height int) to specify a desired
|
||||||
|
// window size or else the default will be DefaultColorPickerSize.
|
||||||
|
func NewColorPicker(config ColorPicker, dimensions ...int) (*ColorPicker, error) {
|
||||||
|
var size = DefaultColorPickerSize
|
||||||
|
if len(dimensions) == 2 {
|
||||||
|
size = render.NewRect(dimensions[0], dimensions[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate settings.
|
||||||
|
if config.Title == "" {
|
||||||
|
config.Title = "Select a color"
|
||||||
|
}
|
||||||
|
if config.Supervisor == nil {
|
||||||
|
return nil, errors.New("a ui.Supervisor is required")
|
||||||
|
}
|
||||||
|
if config.Engine == nil {
|
||||||
|
return nil, errors.New("a render.Engine is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the colorpicker window.
|
||||||
|
window := NewWindow(config.Title)
|
||||||
|
window.Resize(size)
|
||||||
|
window.SetButtons(CloseButton)
|
||||||
|
|
||||||
|
w := &ColorPicker{
|
||||||
|
Title: config.Title,
|
||||||
|
Window: window,
|
||||||
|
Supervisor: config.Supervisor,
|
||||||
|
Engine: config.Engine,
|
||||||
|
Color: config.Color,
|
||||||
|
OnManualInput: config.OnManualInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Handle(CloseWindow, func(ed EventData) error {
|
||||||
|
if w.cancel != nil {
|
||||||
|
w.cancel()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := w.setup(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Window.Supervise(w.Supervisor)
|
||||||
|
w.Window.Hide()
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then is a callback function when the user has chosen a color.
|
||||||
|
func (w *ColorPicker) Then(callback func(render.Color)) {
|
||||||
|
w.then = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnCancel is a callback to handle the ColorPicker being dismissed by the user.
|
||||||
|
func (w *ColorPicker) OnCancel(callback func()) {
|
||||||
|
w.cancel = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup the main UI of the ColorPicker window.
|
||||||
|
func (w *ColorPicker) setup() error {
|
||||||
|
// Load the color gradient image. Guaranteed not to error.
|
||||||
|
if tex, err := w.Engine.StoreTexture("ui.ColorPicker/spectrum.png", MakeColorPickerGradient()); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
w.tex = tex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare values for the currently selected color.
|
||||||
|
var (
|
||||||
|
OriginalColor = w.Color
|
||||||
|
CurrentColor = w.Color
|
||||||
|
)
|
||||||
|
if OriginalColor.IsZero() {
|
||||||
|
OriginalColor = render.Black
|
||||||
|
}
|
||||||
|
CurrentColor = OriginalColor
|
||||||
|
w.selected = CurrentColor
|
||||||
|
|
||||||
|
// Divide up the main frames.
|
||||||
|
var (
|
||||||
|
btnFrame = NewFrame("Buttons")
|
||||||
|
frame = NewFrame("Main Frame")
|
||||||
|
leftFrame = NewFrame("Content Frame")
|
||||||
|
)
|
||||||
|
|
||||||
|
// DEBUG
|
||||||
|
// btnFrame.SetBackground(render.Yellow)
|
||||||
|
// frame.SetBackground(render.Red)
|
||||||
|
// leftFrame.SetBackground(render.Green)
|
||||||
|
|
||||||
|
// The main frame vs. the button frame on bottom.
|
||||||
|
w.Pack(frame, Pack{
|
||||||
|
Side: N,
|
||||||
|
Fill: true,
|
||||||
|
Expand: true,
|
||||||
|
})
|
||||||
|
w.Pack(btnFrame, Pack{
|
||||||
|
Side: N,
|
||||||
|
PadY: 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
// The left and right frames of the main frame.
|
||||||
|
frame.Pack(leftFrame, Pack{
|
||||||
|
Side: N,
|
||||||
|
Fill: true,
|
||||||
|
Expand: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
////////
|
||||||
|
// Buttons frame.
|
||||||
|
{
|
||||||
|
for _, config := range []struct {
|
||||||
|
label string
|
||||||
|
callback func()
|
||||||
|
}{
|
||||||
|
{"Ok", func() {
|
||||||
|
w.Destroy()
|
||||||
|
if w.then != nil {
|
||||||
|
w.then(w.selected)
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
{"Cancel", func() {
|
||||||
|
if w.cancel != nil {
|
||||||
|
w.cancel()
|
||||||
|
}
|
||||||
|
w.Destroy()
|
||||||
|
}},
|
||||||
|
} {
|
||||||
|
config := config
|
||||||
|
|
||||||
|
btn := NewButton(config.label, NewLabel(Label{
|
||||||
|
Text: config.label,
|
||||||
|
Font: DefaultFont.Update(render.Text{
|
||||||
|
PadX: 8,
|
||||||
|
PadY: 2,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
btn.Handle(Click, func(ed EventData) error {
|
||||||
|
config.callback()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
w.Supervisor.Add(btn)
|
||||||
|
btnFrame.Pack(btn, Pack{
|
||||||
|
Side: W,
|
||||||
|
PadX: 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////
|
||||||
|
// Left frame.
|
||||||
|
{
|
||||||
|
// The gradient image up top.
|
||||||
|
gradient, _ := ImageFromImage(MakeColorPickerGradient())
|
||||||
|
leftFrame.Pack(gradient, Pack{
|
||||||
|
Side: N,
|
||||||
|
PadX: 2,
|
||||||
|
PadY: 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Below that: your Current Color | Original Color indicators.
|
||||||
|
var (
|
||||||
|
previewRow = NewFrame("Color Preview")
|
||||||
|
curColorFrame = NewFrame("Current Color")
|
||||||
|
origColorFrame = NewFrame("Original Color")
|
||||||
|
previewDividerFrame = NewFrame("Divider")
|
||||||
|
hexLabel = NewLabel(Label{
|
||||||
|
Text: " Hex color:",
|
||||||
|
Font: DefaultFont,
|
||||||
|
})
|
||||||
|
hexButton = NewButton("Hex Button", NewLabel(Label{
|
||||||
|
Text: w.selected.ToHex(),
|
||||||
|
Font: DefaultFont.Update(render.Text{
|
||||||
|
PadX: 6,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
curColorFrame.Configure(Config{
|
||||||
|
Width: 24,
|
||||||
|
Height: 24,
|
||||||
|
Background: CurrentColor,
|
||||||
|
BorderStyle: BorderSunken,
|
||||||
|
BorderSize: 2,
|
||||||
|
BorderColor: CurrentColor,
|
||||||
|
})
|
||||||
|
origColorFrame.Configure(Config{
|
||||||
|
Width: 24,
|
||||||
|
Height: 24,
|
||||||
|
Background: OriginalColor,
|
||||||
|
BorderColor: OriginalColor,
|
||||||
|
BorderSize: 2,
|
||||||
|
BorderStyle: BorderSunken,
|
||||||
|
})
|
||||||
|
previewDividerFrame.Configure(Config{
|
||||||
|
Width: 1,
|
||||||
|
Height: 24,
|
||||||
|
BorderStyle: BorderRaised,
|
||||||
|
BorderSize: 2,
|
||||||
|
BorderColor: render.Grey,
|
||||||
|
})
|
||||||
|
previewRow.Pack(curColorFrame, Pack{
|
||||||
|
Side: W,
|
||||||
|
PadX: 4,
|
||||||
|
})
|
||||||
|
previewRow.Pack(previewDividerFrame, Pack{
|
||||||
|
Side: W,
|
||||||
|
})
|
||||||
|
previewRow.Pack(origColorFrame, Pack{
|
||||||
|
Side: W,
|
||||||
|
PadX: 4,
|
||||||
|
})
|
||||||
|
previewRow.Pack(hexLabel, Pack{
|
||||||
|
Side: W,
|
||||||
|
})
|
||||||
|
previewRow.Pack(hexButton, Pack{
|
||||||
|
Side: W,
|
||||||
|
PadX: 4,
|
||||||
|
})
|
||||||
|
leftFrame.Pack(previewRow, Pack{
|
||||||
|
Side: N,
|
||||||
|
FillX: true,
|
||||||
|
PadX: 5,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Bind clicks into the spectrum image to update the selected color.
|
||||||
|
gradient.Handle(MouseMove, func(ed EventData) error {
|
||||||
|
if ed.Clicked {
|
||||||
|
point := ed.RelativePoint()
|
||||||
|
|
||||||
|
color := render.FromColor(gradient.Image.At(point.X, point.Y))
|
||||||
|
|
||||||
|
w.selected = color
|
||||||
|
hexButton.SetText(color.ToHex())
|
||||||
|
curColorFrame.Configure(Config{
|
||||||
|
Background: color,
|
||||||
|
BorderColor: color,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clicking the original color resets it.
|
||||||
|
origColorFrame.Handle(Click, func(ed EventData) error {
|
||||||
|
color := OriginalColor
|
||||||
|
w.selected = color
|
||||||
|
hexButton.SetText(color.ToHex())
|
||||||
|
curColorFrame.Configure(Config{
|
||||||
|
Background: color,
|
||||||
|
BorderColor: color,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clicking the Hex Button prompts the user to enter a hex code themselves.
|
||||||
|
hexButton.Handle(Click, func(ed EventData) error {
|
||||||
|
if w.OnManualInput != nil {
|
||||||
|
w.OnManualInput(func(color render.Color) {
|
||||||
|
if color.IsZero() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.selected = color
|
||||||
|
hexButton.SetText(color.ToHex())
|
||||||
|
curColorFrame.Configure(Config{
|
||||||
|
Background: color,
|
||||||
|
BorderColor: color,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
w.Supervisor.Add(gradient)
|
||||||
|
w.Supervisor.Add(origColorFrame)
|
||||||
|
w.Supervisor.Add(hexButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the widget.
|
||||||
|
func (w *ColorPicker) Compute(e render.Engine) {
|
||||||
|
// TODO: free the w.tex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy the ColorPicker widget. Call this instead of Hide() if you close the
|
||||||
|
// widget programmatically! It will free up SDL textures and so on.
|
||||||
|
func (w *ColorPicker) Destroy() {
|
||||||
|
w.Hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorPickerPreset shows the preset color buttons.
|
||||||
|
// Suggested to have 18 colors which is displayed in 2 rows of 9 buttons.
|
||||||
|
// More presets may need larger than default window size to fit.
|
||||||
|
var ColorPickerPreset = []string{
|
||||||
|
// Row 1
|
||||||
|
"#FFFFFF", // White
|
||||||
|
"#CCCCCC", // Light grey
|
||||||
|
"#FF0000", // Red
|
||||||
|
"#FF9900", // Orange
|
||||||
|
"#FFFF00", // Yellow
|
||||||
|
"#00FF00", // Lime green
|
||||||
|
"#00FFFF", // Cyan
|
||||||
|
"#FF00FF", // Magenta
|
||||||
|
"#FF9999", // Pastel red
|
||||||
|
"#99FF99", // Pastel green
|
||||||
|
|
||||||
|
// Row 2
|
||||||
|
"#000000", // Black
|
||||||
|
"#999999", // Dark grey
|
||||||
|
"#990000", // Dark red
|
||||||
|
"#996600", // Brown
|
||||||
|
"#999900", // Gold
|
||||||
|
"#009900", // Green
|
||||||
|
"#009999", // Teal
|
||||||
|
"#000099", // Dark Blue
|
||||||
|
"#990099", // Purple
|
||||||
|
"#9999FF", // Pastel blue
|
||||||
|
"#FFFF99", // Pastel yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the color spectrum image.
|
||||||
|
func MakeColorPickerGradient() image.Image {
|
||||||
|
data, err := base64.StdEncoding.DecodeString(colorPickerGradient)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("ui.MakeColorPickerGradient: %s", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// debug
|
||||||
|
// ioutil.WriteFile("gradient.png", data, 0644)
|
||||||
|
|
||||||
|
image, err := png.Decode(bytes.NewBuffer(data))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("ui.MakeColorPickerGradient: png.Decode: %s", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
// colorPickerGradient holds Base64 PNG image data for the color picker graphic.
|
||||||
|
const colorPickerGradient = `iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAIAAABM5OhcAAAABGdBTUEAAK/INwWK6QAAABl0RVh0
|
||||||
|
U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAACvxSURBVHjaYvz//z/DKBgF1AYAAcQ0GgSj
|
||||||
|
gBYAIIBGE9YooAkACKDRhDUKaAIAAmg0YY0CmgCAAHRXSRKAIAxrHP//YC6RpS2pqAeQpGt6gdta
|
||||||
|
s35/B8YJAa8b/QCDmqjvBa9gfshNHwegu0DSuDUd5zQrXpl+4QiDajTAWK/8X5QFm6HEfsdApTIF
|
||||||
|
onjtOWNRCnXlzvaE+rRRpXkI2Ske8o+52Y/8kCop07A4Ygux16IoS1c450ap7O34CMBGGSQBCMIw
|
||||||
|
MHX8/3e5EUcpbcJ4pAIm7YYbY3wvV+SBUG7YAq34LQO+Ad2YAiPgklsjbS7vARVb3KwxT7mOTd4v
|
||||||
|
Xpf/u5C6vFX5SSDrOjsSWjktxY4CVAcFHXF1OC//nMbEWip52vuABbGxg0xAIl/PBWm2CQkKHSOd
|
||||||
|
QPpWxUyM8tINgM3zLT4CEFJFKwCEIKz1/9+cx0W15YTABzUDN6dbWGxNHMoo2aQaVT18TZpwfg4P
|
||||||
|
HQkRbDwV/9ry76qV4EJBFAYJE84CgIWLe1ebQzpUZ7uhrukMezJIfSqs78tUj6jMSLu4xaeHMHJS
|
||||||
|
t8FH9LTGocf4BKCzCnIABkEYmP3/uR7pNlQozl0MIiYWSvGS3pN3TektHtS81cM57Yj0ixmAhUmL
|
||||||
|
0hKTY6I9qyVjBqXElSk9bMjUrbLiQC8hDLFtcuoPwqP668S3NOC5Hg1hizor1a/8LGCZf3O/1AAU
|
||||||
|
Ldg7HTQhpFSpAONTkAIoIUSKFn0LQLJED51FC9t2eyBhGCMLKrcAbFZbDoAgDGPV+5/WX4fGrdoS
|
||||||
|
E0MQ+Vgfm93HcfwTjqCB5tjKdoPP0//B1yQ+cC5o7GEz15Ew3EMohfnsTWTz/91MMaI48ptndDSm
|
||||||
|
jVBMriIElk9Pc7yQUvTCkhY9k3XRZixH0puUmwRTujTgeznXzg+S+vqpRKjzjb8NODCogD4EEGsk
|
||||||
|
9ow5DYDWakiq9PMfIeW9BCC8DHIABkEgKPj/J2NtaKssBUw4GC66GRwjDtY+ow7TniSt3tLOo+xF
|
||||||
|
gL18nTTHZ6MB531LomZIylrN7A33I1oc68IY7L4RWaRW5rlZUJ0KX0znpCVSS4YUAhkaFIVho0D+
|
||||||
|
f4t8pIyGYEcKSlMARsxoB0AQhKIx+v+f9TUlpUDi2urFMeccV+9B5r6V8nzjjAVWmPvIljIGLczI
|
||||||
|
1WLQvBcJzfH7FdTb/OSZHlpbf+pwm9LCUn7yiALDiAv8R6ZpPet7eY9NGXI8/BgsY5QUmKdPJsxh
|
||||||
|
U4PSzSBDzFub+Sy7SvwJT0pC3gOLutaTiRm19xSADivaARAEgWL+/xdTtDgOodbGHLkeDjk8EMSy
|
||||||
|
fbcmn551bFtgmG8ubgLyUQdZ6dK3J8HalXfgQtTPquSZEi94dobjqCNEu7akZrc6qW6Amw7EYVnL
|
||||||
|
xdessCqVaUp7CinzAuMRv5CGIzZlWSAYjVADdH7yT2OaoDDCQQY5maz3Vx2sn0TBLvFceeN/vLJB
|
||||||
|
1W9vJ6M2uoU0QSZ1rEQfjpaoGA/P4haADCvagRgEYXLj/v9b93xxvUwo1C0xhrAXa1uG+DhPkRSN
|
||||||
|
kAJi8F0QPWJbhQq3vNJhlrVqsFG8+P/Hc+JwNQuhpLx23Ze8ghSTPfIBugQXIosH44cN66FEEMNB
|
||||||
|
VXmBkYyX4eyOh84X2OwCj+kcXk3VbBhWdBQpv/5UvIy6iLmVrgRgrapQkr8tT+On39FgwLexy+QC
|
||||||
|
KwPbXVLayr5XOdHT7yvzszXHcvEXgFCr3QEQBIGa+v7v6l8jawR4SFsbP6423V0gH66ael9ekLOg
|
||||||
|
fM2GgsarWpKomiGx4pz+140QOAKNmaaoAyJPABYfXy4kS+WVXcsmjAOrQUgVVWWU69fArlUFR0IU
|
||||||
|
Q549g+fj59O9capM0rVS1wGdk7nl8J5B/G5gR6SCBrzQyvAYfmuZoekgT3qomSTDtOm5BWDDWnYY
|
||||||
|
BmEY6cT//y+s9dQ8HCPtUlFBD8ax4yYcq9TxQbpUlpElBdPXE+N2LlIOKK8SYd/1AwNJXS1vo89+
|
||||||
|
y5l2qvddL9f2bnXYkt04LN+mzqMz1oCAja9lHHjcn5IIlpRDeoqLcc7SOVC8CirLzuxMJnjEgf3y
|
||||||
|
LS58H0/uYreGwtJoXRFXriERqgLIH3JGoppkBt1FIAlFpxI4YjDEq9hU2LYVyfLXAFBrEC094QXz
|
||||||
|
E4Auc8kBEISBKGjw/tel0SqCHfoR08SNJAyFNy0KsXa9//seAqJaHKpia17B9w+uUDSHyyU8HGos
|
||||||
|
dxxIHQoJzH1+5uh1zZ59W/CpJKOn9NHAr+ky9Z8EWXxO8V4O1sHBB0+rhyQp1NcfIsnqVxzObCoq
|
||||||
|
BycnBkYCMavkZDHWLzkZr9hcaQKN6dapB/HqqYZkMuoRgCtryWIQBoGlau5/3FobaZ1hKLjJy9MN
|
||||||
|
BOYTgsZahIsNiBj2P/9Rch6Eg1F4xBGId+IMJn2jwnvqxmE5Ug97KoRnLS5E9KhvX1zsFWsRR6fT
|
||||||
|
LxejKhRbXcW9Q7W4GAvN5EiMp3wI5ETJFBee7d20THtT3T09CeL+9ZBlPtr7O/4S9kHaKSbSxEWO
|
||||||
|
KijX7shYUagPNqxJchWBn887af9Pi9EQi+PWdZCD52ZVOkSYg78AlL3kg3I99ugtZPIVgCoryWEY
|
||||||
|
CGF0mqT9/2uzoFINYPBcIo2UAxbGZnErjImvVDWJ5YHfJt9XywId8PqGODByMbcGhBh/Wu/dsgFX
|
||||||
|
Mr4Qp3tGx+nI50NUU5T6AwPR7nezG6H+afekBJOCWEfgAaU+0neouifGBvUdrIKp23J4tnVp/ltG
|
||||||
|
qKoVcwwTwOm5qMq/Wp+DXtZJ8VEKYHbJ8Wmz9LtCog7m4DUFIQmVUtrIDyjDWC6ENWaxoxQS+MSM
|
||||||
|
/sqvOL3EuVUMa7+ZYP4CsGlFKwCCMNAV6P//bWW5yLbrnIGESA/KbnfzZjdIFx1ItnQpzP2fArEh
|
||||||
|
BUwkfOBrQOoiJ0Kt0aV/pg+8Bs+LF/myWzhsYOUgLp4rYAfWSrUU0rs6/ebRQVKSha8pkoyBT1L3
|
||||||
|
FN9uyCSFIRyVGHifBrAV5P+0Sy/LOUtexjHkSQ6utHV8diHonniIWjgz9b5poY23wlCPvPve/Hv4
|
||||||
|
PGCr3gLwZQU5AIIwDIIZ/3+tRAWFMEY3iIkHDh5WWGk7hhSKbhOcQsSS4S1UHkvEz4iqBS3vvwar
|
||||||
|
wN1/zao7CyaCXvu9LR+cVlGGRFqqkjy+7UfyrBUi1U5TxAwjA1wEWU+2Xg7rxpkYSKaxxlk0eOcO
|
||||||
|
FQTG3ljHAoZ0rKSxmeiZ/DIXzrqryj4POoWEUxZ6LEORxF9jfdIsqevnE4BMK8FhEIZhysQY//8s
|
||||||
|
GhrtilO7LvwgIW58BGwsAatVemBXJf31r5qkZufdEXeR/8+bVfTYzU/FXZlY5q6UZ3q3KLwCVSF4
|
||||||
|
2Q4bjTLo0sZ62yxW6vRPuj96wJQZOmfqSqvs+oIGk71i78J+Wql2cn6gSi11/j6IJIygASvYT2eS
|
||||||
|
LwkHBquNdSEPpkJZQSQ/PJEt7lpXtsLPzy/jkpGRVYqwkAwO0+9lTlAKvaGEoeregSq2NDVzwesv
|
||||||
|
AJtWkMMwCMNCS///2mpi65YKBweL7tYLFUFx7Dipdp49OQ4QyDtQUKhAOL5QWzv9953+yRcRH+u0
|
||||||
|
64GLmIPnfctPjGjtXFm9nHW2QJ/Y6x+BhErD3zZtxsmAHeR4yauMfHLZ3EpDVeO5IJQdT1Bteirz
|
||||||
|
Y64EukyUxU8f1B4hPaHeBtStET3Uwf0U9MHGRmN38nexDxRv3EytHuNYOvBeAYsadCnJtHK/Lxtu
|
||||||
|
pPZ0bFP6jbqVzSByyNpAvUvp6kd6Kb4FoNMMdwAEQSC8VfP937a2Ugo54cjaWutPy1Q47pO+sVwE
|
||||||
|
K+X+YeiolhKU+UbrAswKcHw6SQ1EYqNvgUpSRAhF+EX+6bUiron9ztYKtXKDHyx9bksm2ZztQ+9G
|
||||||
|
fjrpOcKF2Cmkk/pRM7aeBfGHO7Asat46UAfrFjzAg55XlophmfXTVTWGKAA+MvWjeFViH1z5PE8Q
|
||||||
|
/ht5EKHaPQN47/hpX3/CuIEEUa8dUd9Nyi0Am9aWAzAIwiRbsvvf1iiTJdQimv374UDK+vA8FsiG
|
||||||
|
Orwq2VKho4y8Bahf4zion1QiFkC2EKNxXzuyiiuGgETo4B4EVe9TCLVDKgHC1jQjGBPC23yUY3IQ
|
||||||
|
LT/PXFhSkeTPxngThLFeZiPYwVAijG/LMZypOGwPyOWbr56NkIzDiRtaJX+P/0o/f40llgyv9+O1
|
||||||
|
fPforuT5sM0Bjf7cxKqSPgaXh59IGd72tMzBspK2K33teGsEKvQn1mL7BODSDHYYhkEYGrTl/383
|
||||||
|
a5amEmDj9Fz1UFEwPPvbxij4vL3hGRClIzJbyuV0ke96sAMrrnMb03j6EWlPGV9FTtI3pNMcKklY
|
||||||
|
PYv7GsfVD09DQPwVi50ktMJq49Pw3AdgiMNpAmLfO/uyw2fLnUSKUliRUe+K5t6Sylg5+LfAIAMp
|
||||||
|
2XIbJmIZ+EInqPyxoiyd5zg63SQaF1vuHxMum4PMEaYB3UNTx+QthaweTd3T3eVhUpbOle3S0CW+
|
||||||
|
JD8CEG5GKQCDMAwV3P0PPFcqKOuapEPw04+K0PqSuD5TZFqIwRedr7+c+KAdRuJBkzgritV+ijBZ
|
||||||
|
egEP9NyIx0kA3hv847i2yqUITR4Ul7h0htjqqNq5PBaLPwecF7PKQBQbkWFkNwWDMewSm+liodLl
|
||||||
|
lPmiKPP/OBQNswp3Y6lYElR4TwEot4IcAEAQNPv/n/HSQUVYdWq5AXFj6tkpoC9rUOWB9oIJMXO9
|
||||||
|
woxSUBVSwpASVq7/Nu8XwSENQmrD/56gzr6wtvG80KKnSGtL3bVACY/e5PuSAvBtBjsAgCAIlf7/
|
||||||
|
n+nSId2Ta2uQIm21cVoPQydFvCZYJUfgqUPxrvkgFhEKCE2oitIU/B1y5fv26hd2HmxT1ME1cmYD
|
||||||
|
T1EBNKeSSiYv/3kLLcWgFd/KFUBMhN2FP1sxYoz4YsoSVz7gseQ/3kKTAduGRhxF5H9sXEaMbVRo
|
||||||
|
iRm/yTgiiMhy8z/2/Iw2lc+INysyYGS8/3hrg//4ghTTjP9ElzQIABCAcjM3AgCEYRhm/51NRcHh
|
||||||
|
BzbIOUZKwxTR5Y1I5sCog6/oyCMjKnIngQb2UEu29sKeBTgToHpyVXUU0cC3fvzQSxrw+v4Fj4cM
|
||||||
|
AQ3HJQCjZm4EQAzCwNvrv2c59TCSERXw6Av4k395IOBIxOJtdKpHAQErvkyekxlmD0EOVexzKGBI
|
||||||
|
QcK/ZwtFrTlR3QmuB6GxN4W4oZxEGeZ1BKDkjJEAgEEQdvb/f47dBVpHFwfwNCye3UpBqROG+6UO
|
||||||
|
sdNWXL9dpc8VT8CEOsNNNQgIH0/4cNsHNvxJZj2fkh8mYxE91l8HbtkCMG4GRwDAIAjT7r8z/bdE
|
||||||
|
WcFTYh6c6AMqwNyzx9rCj5XHuk873efZrQQU4Eyf0ZUpYyfHkfse2edouZ2xZH7kofXVY4qI40c2
|
||||||
|
K68AYiLQuiWy2U58OGJrjf4n1IXB35L8T1S+xdM4weoWRmz9o/9EjQoQ3x/C2sYiOm4ZsDQ08RV1
|
||||||
|
DPjacvhjkZG4thdCDUAASs7ABAAYhGHL/v/ZPWC0O8GCNRX0Nvwa7mDaM4p5/ggBIWxcju1MKlSQ
|
||||||
|
MisL5u6I/FsXHgbZlUf+Lxy3DtJOb+Vn49up1CcApWaQAwAMgrB0//+znhcUti+oKQ3xZBvA7pM8
|
||||||
|
1rLgNywm5Y9cHpJfRkhjtpX8pExB9WiC3rEmUGJr5K2XHXnCQi8+uotKVngNuQVgxA6OAABhEAiK
|
||||||
|
/feMfydcaAFjJuyNm4EpM4Hd9EyppKkYuOZiKEzh6zVCphempmWTKAszhIbVx/iL3ZnK2ag56EQB
|
||||||
|
LkMQfgJQYgY3AAAhCAu3/851AUDPtz40TWx4sVPuIlQQQh5TJJUKaqFNHnsdkqqvLJj4InWYWP8q
|
||||||
|
iwri5Im8oQ6kbDmigtyQ4TA1AoiJcDASP+z9n0CpgndiAXsOx5PhSGyNMGK0xxlwN3kIlj+ECkci
|
||||||
|
21skNtT+EyhpCNQ9DDiLXFxWEdOsxR6HAAHEhKVyJjJ7MxKX/4nujTPiGM3A395lQDuEC2c3iNQe
|
||||||
|
LKEZQ0YiBtH+k9W0/4+vo8mIo7/EiHf6i5EBZyMU7ywqMWOV/zHqNRAJEEBMOCtzRuJCjQF3i5GI
|
||||||
|
pi3BZQv/iR79JNQxJqldTXSPCrO4YMR97AD+1gBqyP/Hl3/wxQwjofBkxN6C+U9EtPzH29FB6dkC
|
||||||
|
BKDEDG4AAGEQCO6/Mz61DxA3aNrQHLceBZMFB6LArxoq8XPeOTg9ReR2xZqJrgw5xGxck8yfiZ4t
|
||||||
|
S2tEHlGSMfJVSiYQDsLO8rcAYiKQ/gmuayG+liS6qUb84Dbq6M9/AnFFoIH1nwFfcOPuE/3H2ykm
|
||||||
|
6HMcs2T/sbUKGAl1ZRmJa9FhaMczHPMf93jifxyZEyQFEEBMhP3JSNzY2H9iSyas8YY/iTKSYOd/
|
||||||
|
vCOY/4luY+HpXTPiLEsZSVlxRERTneBiqP9EzT0RHLvCn2LwV4WYDXOQIEAAMeHMXv8J1UD/ScmX
|
||||||
|
eOsNgoOveIZEGFGi+j/u6p/ISv0/gRoQuUj5j3eImOCSWVzuYiRcZjPiGKBjIHoGGnWM5j+2updI
|
||||||
|
I7HXNAABKDUTGgBgEAau8++5CBh95gBICOSuN5wLpBAG0pdUrC6/SnYTh+7EJwOMcNvI8huAYOfc
|
||||||
|
IlA5C5RrckQoQIfwidzkEDoKKKsfARg1gxsAYBAElu6/M/5bEDYgxqgc3rf4TVzIQniC4vQXf4kY
|
||||||
|
zTPWSUG9271MtAB6xpVdtcCSTynUgmWC+T1QMTaDKymFcqhwd8AIICZ8RT4D3klzIksbRgLKca2p
|
||||||
|
JWZ1OGrawr8eDX+fjRFbEU1oEeN/vH2A/4RmqXCPH2G9YIERW4ORuC4Tfo2MOKICT0OH8A4IgABi
|
||||||
|
ItwZJ347ChETfozYdDMS0VYkuhD+j3eUlhHv4Ceu4XBG7MMNjNiSPK7hfaxDbIzYho3+YzHpP47R
|
||||||
|
Y1InCv8TVQEROSiGK2BBXIAAYsJShTESkdWwrgAgboj5P44YJr5xQEghroEZRhIXWmKOKeNdNoW/
|
||||||
|
QiZyUTpqicVA9GIokprtJCohuOILS1oECMC5GdwAAIMgULr/zvRfL2i6gTEEEfEAS3qyftSyzqPR
|
||||||
|
OakexScYTZaUXgY0ZUS9K6qIUhtYtNv+cmdM1GTWlziwVZ8wKcgaBWsup4/gYnQFEBNpQYB/SBDH
|
||||||
|
2Pl/4vqDRA404u4b4ikzGUnZxYqnEP6Pz7kEV7TgH4ZEDTGsdR+e1v9/ojvXjNg7oAS7SaSNYQIE
|
||||||
|
EBOBfPafuN1YeDvPZI+n4lm4i6MpRnBOluAuNqJXHOIpbIlsYzFgzrDh3J5M2mIo4hbsoI44EJyV
|
||||||
|
ZSSiEEAAgACUm4ERACAIArH9d6YBkjdX4FQU5JAuruW+VWuBlFlFIWmW7xPTNd1D1noXCQ6ca6zz
|
||||||
|
e3ISi2hjXE9+evwtfhPwJfrRHDJmVwAxEehREdwcx0jmUhmsAzD/8Y5y/scdiRgjPoyESlf8aQ6/
|
||||||
|
dmyp+z/uuVqCJmEbrMU6Xkl8952BhNIGfynOgGMgiBF/6xcggJiISgK4enH4t19ia2Ph2olL5NkA
|
||||||
|
uCoiRgJdTJJaurivpsRf9+CaKybY48Sr+D/eley4Np4Q2YVgxNk8ZCBcPWBJHIhSHyAAJWdgAwAM
|
||||||
|
grDx/9HsgYr4g0HAxgH0a7KpLifdjWXxTe/QOinq6ounWPzd0HHhCNHlZWYqI7X0yY45QZ2uk5fU
|
||||||
|
PI2l0YEG9wvA2BXsAABFIL3//2eub1PpbuOU1PBIyVS+Nk3eLP5+v8CM8wBNvA2RZkymL3GhQ+FY
|
||||||
|
A0uRSxEIoAjZVLCOBWAZ1sbvVKmUM3r1g4oW01jACMDYGdgAAIMgbO7/n9kDtuwEo1FAjPeXDNZc
|
||||||
|
xFAA2VFpbsmgiL06HoDWVY8R+MPMIavmDK0ooJ3Cwe5aUi7sjZrRTmGoddMtSGsJ+AlAyRXbAACD
|
||||||
|
IOn/P+PaJiJ0d9OoIHjmkkameUeAUhANTAtw4OEkBec+KuQQ9LA7s5Tnz9IqTzq6oaRbl8G2sACg
|
||||||
|
+lARMPasYn2X9MS3AGLC3sf8T8SZFv9xl1tEFAU4T+wirsmNw1pGovbpEeiVMBDq/vwnKgL/457V
|
||||||
|
IXE3FsHmLvHhxoBzdPQ/EUeLESwiECkSIICYcA4A/cfbsPuPo3PEwECgf4Yto/4noRuOJ9QYcddM
|
||||||
|
DNi6VsRvvSY0asaA82w+Aq4gopL7T8qoOpFb2BiJGpkn8kwszFFAEBsggJjwVdq4R2oJjAQRUXv/
|
||||||
|
xzvKi6smwduvInjEI0l9UCJGKAm2dxkJDaIyYFtsQ6gu/497PJFgIcCAcRwWA86UzEhU8xlHagMI
|
||||||
|
ICbSAhzPCiTcSRPvqV/YG8aMuCdlGHEWEwQ3DBOz2hX/fAmOQpgRb+ce/3gZcXPY/3FP+hNTSfwn
|
||||||
|
eSkEI6FKlcDYBEAAMRHoM+DfPMiId2/yf3yN9/94lyTiGj/Be+LBf0LrBYmpTxjxtpKIarMQFfi4
|
||||||
|
Jif/Iy54xTNLgX+/ASPunIGjDP2PN06IXPyOEoEAAcREYPARV44gxu1EnL9EzJZwolffMuLoRDLg
|
||||||
|
mLdjICI74q5eiTzHlgF3dYV3XRUjjqE5RrzzzYxE7HzAtvmH4IlnRA5aIyITIICY8JQA2B3OgHun
|
||||||
|
NQ4n/ScxxWAd+mAkMKBIUkAw4q3pGHAXkf+JLbfwHNdE3ElCxCziZCTuvBAGAgPx+Jd4kTcnwgAQ
|
||||||
|
QEw4KxhG3IPsRLZ3ifMwwfkHgqcv/Mc/pEygb4LZJsaaTHDv7fhP9PAFA+6jSYircomZMSBymSoD
|
||||||
|
yp0hDLhPFCJmBhJLZQcQQIS22BM8TZqIqPyP40AD4ke+GQl12BlxVvYEO9JEtvdxJ2xGQgPIxLfJ
|
||||||
|
GAgfhUZk34CBiAqekShVxO/gRvEwQADKzuAIABgEYdL9h3YAyUFn8MFJEB9KFFOyyjOo/wY0DH2g
|
||||||
|
mPhoiz2quR6dmOpQuq44Z1PCgjRYffQhThGGiqTNucoEZ5TWTW81rQBc3cEOwyAMA1C8dv//v2tp
|
||||||
|
OpLYTXpDIEUYOHAAvf0YFPjq6jjkE6KV5CI5UMG1C6QfVJ0T79oGHERaPpTXnaBK5HZYuJlyz85i
|
||||||
|
G0/KbqKzrfhVYqYfdtf6pR0SzqtwPB47VFiiUT1P14ox8M2JGN2/sOEAfhwvkVIoQrGeLIdE01VR
|
||||||
|
e3oR0dqC094KXzsJoJGm2VvraRqypaMW8PnKsDcE3Rx4WtDYx/OPfCQwHUH9cU8OMIklI3rhTT8J
|
||||||
|
qDOmuuTT/Ru3AGzdSw6EMAwD0IkE978vf8axExfYRSCVtnG2PATrpOeXnFiBabXlvPwSoMK1tE3K
|
||||||
|
YDailMm6pQMZGs2oTNUOpm574VWtIOItcbGHufv5z4IQlzYgaQ6CS38jbeUoqj6eZGJMCNyKfstZ
|
||||||
|
/q+PRDI9wnSXEfRiIfywQfDojuxJBhrm3CcJMxa56TIrLZNx2MhXkmbVINtYZJ4wlXfNYuoFF0GV
|
||||||
|
a2XLxO3GKXM+xpP8jO8jHn4/ugQg60xyGIRhKOq09P73TYJTPHwPrYTYIEUOfwis3jVBQvQ/uNPw
|
||||||
|
aC4K6HsOqF3JJJfBp+T8qCLPsCLKS7l46FEDhD7TLZSWe0tf+wogu953Z5kF6TwQtPRnL4Minlpi
|
||||||
|
/uwU6B5Bi+jYDxKznZo3UFTiqtlSMuIzVyfikejUrfmYMFNk5dYN3xBiU4piazk+kDzw1LcENUrw
|
||||||
|
6QdWyRlIC7ufGQrOG1daSq6pS701HKOsz8a4bRzYBZ9tFDIXniNT81Yl8sWJeL4CsHUGSwzCIBAd
|
||||||
|
rP3/7zWtoQPskm2bixM9aAgLm4mHF8La/tFdgGBl8L0BC27jA9Bw5GQT8bmAyakpPyULHtMseQ0O
|
||||||
|
IDiOb/rJ7aj5FvbmeNAl406S65ZKDSMvm+PYpeUWlzEsorJw5NXAL47X1vIlSHAeK6QrF38ISPi1
|
||||||
|
a8IN+570/qqSem4C3Pw6X8Lnf3YiBG5it0K0Noi1T+FVPjI2Q+M1MRJU2Bl3KxhWyWUSj0Nq0/5w
|
||||||
|
iN2lmjJfEXwE4OoMkhiEYRhYmA7/f26b4qoklizTOzARtqUEDvt8pZDtNiCebVkH+bhhenPGRSJY
|
||||||
|
kUHRPorR6pYRxM6sNvBxNda7QxShcigla/khamih24t2Cu1PWAUTs/9k4EZ3Rjg6qOqz+unkhKPE
|
||||||
|
qLIUc911zGLNdVwCQOMdmpXRJsatVpuwLgyemJp2+AgAIbQfRlfypYKo4nQsetVXmRHMjPTb2Vi7
|
||||||
|
jmlV5HzCsS4IhYrKMsCdY/VTyei74m4CgDHH/NXyE4CsM8oBEIRhKIt6/+sKBFE32m36wx8Coetq
|
||||||
|
NHl7DZ8RhH/yB7cVQo4ZAjsQ8Qb2JfSzwQdtliFw93dG05VNQ1X3e+pYS6qL1UnEaZ5XSqbwKvZE
|
||||||
|
pN7EG+K9OJQ2oLMTglZbnmxBT+KuO+GH73MOL7WnjB7HGmzt0FPFkfrnSOEwIxSK7fOCb9mliFuW
|
||||||
|
/N58kQ0nQvrS07ZyuqHKl56YeEt2qRj2FV08VShWEKSIc2zlG7kGyMBDEpw3spLLLQBZZ7ADMQgC
|
||||||
|
Ucd2//97t6G6ERgB99JTD6LIm0mazppYh8DCTjieKbHZ8Gez9Xadvsaorh29MnT7MFkvD935V58C
|
||||||
|
B6JXMAtGnpmUvl1pnJN3oLTX39cNrATJfWx1u8dV05VNEhAXS3oIvp6UgW7cZC+uduyeK3+k7n7z
|
||||||
|
tWf8vEcRU1QOFHe1w7Nj8WghRkIq8lgarRSEp2EIERW3F+GB8pOJuG2DJv7jPtF8thgHSRRnIgIw
|
||||||
|
knrLig8hnyy799ZPAMLOYAlAEASimjb9/9eWhdk4Ci5S08Vbw2woLhx8cXdm6GMpzegVWuUXYrof
|
||||||
|
RVZ9TpyF0D0WaXSwygX0VQQXIjaMReON7x8S3WTJsuvE6KzFBLC2dhYroWhsqVoX1tG7iqQEqk6w
|
||||||
|
wmQaRgttLu+20E7YEVud4U8HJncvvPqPF0Jb8I074dgP0MUXXHL9WBwgSUjcksNpc0wyarRHALrO
|
||||||
|
BQdAEIahGLn/fSGIJmzo28cTsNKODkLSJSzHhZtJHBG7yVcmfPjdcGNRKywxhI4bSCSC0dWvjAmC
|
||||||
|
+/ujJ4Zqu4NaSt94OIp8/XGEDrvgG1XByxQ7QtJ2s3R0eMiwXZpwkespZqCzJnJyAkzJaJn6rKXX
|
||||||
|
4LongOLBOCtswDNszPkM+eSK5xGAbHPBARAGYaj4uf95lzExDkqHHsBkDlhf65yN9bt0RjM4mBfK
|
||||||
|
Ohf7IhdC5TEYr/eRfZ62aKyR1m8LLOlCKWwGOJHQQZUyEaNmc8WHfK/p3KR4ovoBW+s+yd9B6ZYy
|
||||||
|
LMzsNE2YVyRk5wwa04Wikncb1KNBUrqU3tLKJ2x+F72SX1v5jl/c1DEJBYGnw8i735+/LJHp5WDZ
|
||||||
|
FXsR9TyDtXmuQg37LEh6w9XTqzEsGsjrshqx5EcAvsxoB2AIhqJ02///7V5QS1BuW5Z4lVS056je
|
||||||
|
73b2zofSwJQiN0mbbEtSEW0XA34ygDU5VhloWXXE0UstmW+mGtw+a2ZjyDo4Wq3foG48FDmCt5/n
|
||||||
|
EmJJYhX9rjUQNibJ2sO8s2GwVVKdBAtQlyQE0j/T0TGvH+YBkQDkKlzbVB7iqi9zLf9SD58AbFvR
|
||||||
|
DoAgCERW//+9pUYzhANz69Ut4o6DwwawpvtGubDFD8u5VjEGwCULI6Hxxb9q6w14xNaiIXUHqZZd
|
||||||
|
+Fl5Q69IBrLgS5dMDgHJU1R1OoWbe79+9oRuaKPsPZkUcKUHlbjNMr0EC5+WDRX16KDuti1I2CqE
|
||||||
|
+ZZsmSMKJgujsG1pePf717LtiXw/kBxVHadI/7XBC9kxHloYz0+ux/MKQNgZoAAUgjCU3/f+F440
|
||||||
|
CMPZjE5Qq7TnhFoZyy4xckyKOQRhBuNCEhkz/XcoAzDaGUvK6LiQbvkzKs7sS1vg1x++EmUE+L97
|
||||||
|
cS5GwpjA9tNBtyU5Mpzom7R0jbedJ2tbCbNhLiy9k6h52QVqkBYHy6JbV2AjZuAOFvColISAKQCd
|
||||||
|
ZoADMAjCwLns/w+eOpalAwroB4wE5WqrolD2KAQ9Wpy2iYBCmnA4MTBq8b6etPfl6BrlatTztDhV
|
||||||
|
+RuVFXBSfj5Vp1slnRJeKcR4IjS4HtCj/YYnVPyt1kNOGEhp1Ux0z8EjxlLRO7FbAtvtU0umq8S1
|
||||||
|
sZnyDsFrUQnW4f503S53w53fhHZx/5f7c7wCkG0GSQCDIAysVf//XwdLpwIh2nsPOEYJG+vC4szT
|
||||||
|
1T6JUZkzlIQ7zIncEHayYn1931arqQ7gnuOQR0VgV4PofIovYLXA72o2EIXf5bvqgFimMJ1xS8Vd
|
||||||
|
VfY/0NyzAwS3vWnUSHtIWEZ/JGjbILLlja/8DkqsBxmi0pL2xze6DUXXncOIYVzIK7k8hYxFSVLz
|
||||||
|
lJS/QVnCsiJYIkKByCCRYUjhzQFbRJb7lfAKwNa17DAMgzDo4/+/dpdmKd1qTNxl50pVUCAG21G2
|
||||||
|
1zynI9UXys916fvnhVGuvdh5jFxfoqdlYiGr4A3pzIzaizddAmWnaZQM5p7kfGrSMd+1KFpxhFEE
|
||||||
|
KcLogoNSTEMTjZy3AIJ+1wd2xFdaTnAI34vAiVVybRcRulGEPkKwn18HrcixdgxVYSLmWI6ptgic
|
||||||
|
E0gyBpd8spTcXUAwqj52MqhrQqHT53nSiQE2C7aZQ9qWZuRIK7ciu98/BPbnj5cAhJpLDsAgCEQx
|
||||||
|
8f4HLgEb5TfaNl22OzLK4PBieLciNLiK1KJ5jjv/8zm9lHArxx381mrj1aGPqFWSzSIAAuJs1XN2
|
||||||
|
dw/5zxTR0RvI0WKYoI84uO9zSKbzsbSan+QdS6kMEQcZq0F2UdL4FTZsOk6EA0o54FwNC8/jJQmh
|
||||||
|
VaawFK3GS8lj2ZXnUsYl7cXfJaYkcZcZMgjrTxfBrpAKpFEAgNxIbgHoNoMkgEEQBgr9/49F24NI
|
||||||
|
UuDi0RkEnLBRxg0D0qGseju1DllAdRtsQgm1IiCcXGSzEmF5Qt/Np4SKLApMhdJ/bNgHUkR5GUNH
|
||||||
|
XEHvruRMlELyPms0+XcJOWwRkqT5VvwAxXtlJLsE7ufj8zzJYVCOxFnkSmEYJ4H6A+xcSs61or8d
|
||||||
|
XwHYNpckAEEYhlrh/ifm44yQ9Ltx4U4aaBqemAp3tdT8AnH4iXSt101Bh/Y+mhBpeP5bdItDFWxA
|
||||||
|
Fo+x6uWq5lptehJ+qdhVUZ5kQiiQBmG9JgtOemI5TtuaabsEbU1erD2qquEP8FUxPrhYB6pxuzUL
|
||||||
|
EjDikMs3qOqQQB1AE31VmihdBEDPYhVmwDkis8UMQpv1CcCnleQAEMKgcfz/j3E5qdCKiUcvpAUj
|
||||||
|
QIoV3hCXV1VlOINAGkQ59M62a+ZI1x4A9I6L19RWLIYcTq7aUtrtL/yvleLF6tzs+c73xB3cALNo
|
||||||
|
mep+GEu2QKuCCbSAIuGAlxZrhJhj1eHgGfNwgcYUgK0zygEQBmGosPsf2WHm2lHI/N3PmoiD9qnI
|
||||||
|
CpNfYmnEvGxfob847u+OOpxWHWdJtGsOPzNZ0JA5Me5HyrTC9JZUR/j3y2sPVn90tsQ4CNIklYfw
|
||||||
|
yoPEQD0ljA/e3ZDs+oCwJ5vuvNMtB73G8PfSiaJkSv+OVCf0q3VGR+poFY9kLbkAvy5Ao3dJwEf/
|
||||||
|
5CcGEUnOxrqnQh8Q128Y/yurGovg+gQg3Ax2AAZBGJrU///kgachpXU7mnjwRSmm1eZjjVIH65Za
|
||||||
|
uescLIoVJNVNF6OmCzZCukS4oW3dcsGC096ajjYBnyQjouYiT8egGLVr9AT+xx3tX1vAotXB18sz
|
||||||
|
SCwDpk1cwWJcYB4p/BGkd+E9S9sCMHbuOACAIAwV7n9lwVE+Lbob4ktpOtjE3G6QVw52go+7O2qr
|
||||||
|
GUe5pVfBT2pGhShJUvzhyDoRYDeGsk9ICyYKs46hk2v6pUByDvbSmYauiQZxuiARQ+/AguGjV4yA
|
||||||
|
JZsfAdg6gySAQRAGIv3/k4WOUiCx9uapoihjzI4rsQgKQTqkFBXPfbb0UujpwGY43+tBGJgI2/9A
|
||||||
|
7WaCvXh6XwKaQEoxXtSmZDyen0wbPuTrHRXZMEZWfU0jsqbTQZvMCcNcgV8ulFjlUp9+WzR8Ejfg
|
||||||
|
vTBOP0xyF3DQSdAKOOfjt+L2JjpqWblj+P9r4mH7g+bAjUY7ZFc9Kd0QqjWpIxBbp2PaHNb3CkDY
|
||||||
|
GaUAEIJAVLv/lddZCK1R2xb68FOxQvRNOUGqhRSwaCKW1xIt8MRIBxceMvJgV+uJEGYNhXSCOIBs
|
||||||
|
aDOcwjXg9+EGz4USFzhyQ6V06nIiPCoyhLr2PAm3HOFSq3F1eYst0e53pbSFH3NvyQLyg5nBk65Z
|
||||||
|
/55Z25S6GSBZxNHXLYPEZ0h7vQKwdW4rAIMwDNX//2c7Bm16UvVBhCFUTOOljUsxRazHMxG5iy9C
|
||||||
|
k5BkzIg8udsr52I7XfWmm/wbjjneRXvoJhyUi/pIC0ttJGzwCFIuEpKrOLWm0TB9sO7/Cc8ZcDy3
|
||||||
|
uaeEBfOGnXjy5LKXqjquse2K5ARCVMegIwefWkL/s6WEf8pVP6yxMIhdmaLfKESvLJ8AdJ1JEsAg
|
||||||
|
CAQl/3+zJFaAGVku3qwCHXcaD0whicIlJ2BjvEWJzbnc8sEJg5d5qBkCmws9lR5ZtMz9IlvzZ9oN
|
||||||
|
1XJRxAoaAtdcdaBudBOyeKXYB3LGSmlynFWz6slCZyFeH8npBO6nEUT+mE2uHmgoycjTdQQwvpyH
|
||||||
|
xv6OggQ3Q5ydVyl2CdE/X8VXALauIAmAEATp//8cO7OTAtq1S5bQlIb+wBqBuVXq/rrpEEzpViuV
|
||||||
|
TA63dQ6QbXIwhTy6tQPWGKQ4p1U5uc2VjCaf7eELkFlzUKQP3lrezaoJVwCB9gZ+WXww+WQlXkBK
|
||||||
|
UWLUUROTSsqthPohsfvWRSgMvXsiDJ1v0ucnhZ0FpgiL7UA+uSJfrGpyfALQce44AIMwDI2l3v/C
|
||||||
|
HehQhO3gLPwmAg+IAvh5L/U2c4LdQY4zckgqFctdLJeMNhDKjTljKP/CMYquKDSyUBWBS8aQsNZ1
|
||||||
|
mEk8YTGYUbaxGUPyg76f5UHgevV7/7aN7RxMSUzNWklLqJIo/4FpUtfXJdyr+rD6b/kEGAA++H2l
|
||||||
|
aiCrcAAAAABJRU5ErkJggg==`
|
19
eg/README.md
|
@ -1,10 +1,15 @@
|
||||||
# Examples for go/ui
|
# Examples for go/ui
|
||||||
|
|
||||||
* [Hello, World!](hello-world/): a basic UI demo.
|
Here are some example programs using go/ui, each accompanied by a
|
||||||
* [Frame Place()](frame-place/): demonstrates using the Place() layout management
|
screenshot and description:
|
||||||
option for Frame widgets.]
|
|
||||||
* [Window Manager](windows/): demonstrates the Window widget and window
|
* [Hello, World!](hello-world/): a basic UI demo with a Label and a Button.
|
||||||
management features of the Supervisor.
|
* [Frame Placement](frame-place/): demonstrates using the Place() layout management option for Frame widgets.
|
||||||
* [Tooltip](tooltip/): demonstrates the Tooltip widget on a variety of buttons
|
* [Window Manager](windows/): demonstrates the Window widget and window management features of the Supervisor.
|
||||||
scattered around the window.
|
* [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.
|
* [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.
|
||||||
|
* [TabFrame](tabframe/): demo for the TabFrame widget showing multiple Windows
|
||||||
|
with tabbed interfaces.
|
||||||
|
* [ColorPicker](colorpicker/): demo for the ColorPicker widget.
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
# ColorPicker Demo
|
||||||
|
|
||||||
|
![Screenshot](screenshot.png)
|
||||||
|
|
||||||
|
This demo shows off the ColorPicker window.
|
||||||
|
|
||||||
|
The ColorPicker lets you ask the user to select a color, visually,
|
||||||
|
using a callback interface. While the UI toolkit doesn't support
|
||||||
|
text input entry, there is a work-around to prompt the user to enter
|
||||||
|
a color by hex code with assistance of your program.
|
||||||
|
|
||||||
|
In this example program, clicking on the Hex color button will prompt
|
||||||
|
you via STDIN (check the terminal window!) to enter a color value. The
|
||||||
|
UI will be frozen until your answer is given. For programs that don't
|
||||||
|
need this, the Hex color button does nothing when clicked.
|
||||||
|
|
||||||
|
## Running It
|
||||||
|
|
||||||
|
From your terminal, just type `go run main.go` from this
|
||||||
|
example's directory.
|
|
@ -0,0 +1,95 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/render/sdl"
|
||||||
|
"git.kirsle.net/go/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
var WindowColor = render.SkyBlue
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
sdl.DefaultFontFilename = "../DejaVuSans.ttf"
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
mw, err := ui.NewMainWindow("ColorPicker Demo", 812, 375)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mw.SetBackground(WindowColor)
|
||||||
|
|
||||||
|
btn := ui.NewButton("Test", ui.NewLabel(ui.Label{
|
||||||
|
Text: "Pick the background color",
|
||||||
|
Font: render.Text{
|
||||||
|
Size: 32,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
||||||
|
colorpicker, err := ui.NewColorPicker(ui.ColorPicker{
|
||||||
|
Title: "Select a background color",
|
||||||
|
Supervisor: mw.Supervisor(),
|
||||||
|
Engine: mw.Engine,
|
||||||
|
Color: WindowColor,
|
||||||
|
|
||||||
|
// Until the UI toolkit has normal text entry controls, this work-around
|
||||||
|
// allows your application to ask the user to enter a hex color code
|
||||||
|
// themselves, using any means available. This is an asynchronous
|
||||||
|
// procedure where you are given a callback function to send your answer
|
||||||
|
// whenever you have it. For this example, look for the prompt
|
||||||
|
// question in your terminal window!
|
||||||
|
OnManualInput: func(callback func(render.Color)) {
|
||||||
|
// Prompt the user to enter a hex color in the terminal.
|
||||||
|
var s string
|
||||||
|
fmt.Fprintf(os.Stderr, "Enter a hexadecimal color code> ")
|
||||||
|
r := bufio.NewReader(os.Stdin)
|
||||||
|
for {
|
||||||
|
s, _ = r.ReadString('\n')
|
||||||
|
if s != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse it as a color.
|
||||||
|
fmt.Printf("Answer: %s\n", s)
|
||||||
|
color, err := render.HexColor(strings.TrimSpace(s))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("%s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping the callback function with our answer.
|
||||||
|
callback(color)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error initializing colorpicker: %s\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
colorpicker.Center(mw.Engine.WindowSize())
|
||||||
|
colorpicker.Then(func(color render.Color) {
|
||||||
|
WindowColor = color
|
||||||
|
mw.SetBackground(WindowColor)
|
||||||
|
})
|
||||||
|
colorpicker.OnCancel(func() {
|
||||||
|
fmt.Println("ColorPicker was dismissed by user")
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Printf("Open ColorPicker: %+v\n", colorpicker)
|
||||||
|
colorpicker.Show()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
mw.Place(btn, ui.Place{
|
||||||
|
Center: true,
|
||||||
|
Middle: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
mw.MainLoop()
|
||||||
|
}
|
After Width: | Height: | Size: 26 KiB |
|
@ -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)
|
|
@ -0,0 +1,285 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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", 500, 375)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tabbed UI.
|
||||||
|
tabFrame := ui.NewTabFrame("Tabs")
|
||||||
|
makeAppFrame(mw, tabFrame)
|
||||||
|
makeAboutFrame(mw, tabFrame)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
After Width: | Height: | Size: 30 KiB |
|
@ -134,8 +134,9 @@ func CreateButtons(window *ui.MainWindow, parent *ui.Frame) {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// When clicked, change the window title to ID this button.
|
// When clicked, change the window title to ID this button.
|
||||||
button.Handle(ui.Click, func(ed ui.EventData) {
|
button.Handle(ui.Click, func(ed ui.EventData) error {
|
||||||
window.SetTitle(parent.Name + ": " + setting.Label)
|
window.SetTitle(parent.Name + ": " + setting.Label)
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
parent.Place(button, setting.Place)
|
parent.Place(button, setting.Place)
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Hello World
|
||||||
|
|
||||||
|
![Screenshot](screenshot.png)
|
||||||
|
|
||||||
|
A simple Hello World featuring a Label and a Button. The button logs
|
||||||
|
to the console window when clicked.
|
||||||
|
|
||||||
|
## Running It
|
||||||
|
|
||||||
|
From your terminal, just type `go run main.go` from this
|
||||||
|
example's directory.
|
|
@ -44,8 +44,9 @@ func main() {
|
||||||
Padding: 4,
|
Padding: 4,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
button.Handle(ui.Click, func(ed ui.EventData) {
|
button.Handle(ui.Click, func(ed ui.EventData) error {
|
||||||
fmt.Println("I've been clicked!")
|
fmt.Println("I've been clicked!")
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
mw.Pack(button, ui.Pack{
|
mw.Pack(button, ui.Pack{
|
||||||
Side: ui.N,
|
Side: ui.N,
|
||||||
|
|
After Width: | Height: | Size: 9.7 KiB |
|
@ -1,7 +0,0 @@
|
||||||
package layout
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Println("Hello world")
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.kirsle.net/go/ui/eg/layout"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Println("Hello world")
|
|
||||||
layout.main()
|
|
||||||
}
|
|
|
@ -1,5 +1,7 @@
|
||||||
# Menu Example
|
# Menu Example
|
||||||
|
|
||||||
|
![Screenshot](screenshot.png)
|
||||||
|
|
||||||
This example shows off the Menu, MenuButton, and MenuBar widgets.
|
This example shows off the Menu, MenuButton, and MenuBar widgets.
|
||||||
|
|
||||||
* MenuButton is your basic button that pops up a Menu when clicked.
|
* MenuButton is your basic button that pops up a Menu when clicked.
|
||||||
|
|
After Width: | Height: | Size: 21 KiB |
|
@ -0,0 +1,11 @@
|
||||||
|
# TabFrame Demo
|
||||||
|
|
||||||
|
![Screenshot](screenshot.png)
|
||||||
|
|
||||||
|
This demo shows off the TabFrame widget, in multiple copies of a
|
||||||
|
pop-up Window widget.
|
||||||
|
|
||||||
|
## Running It
|
||||||
|
|
||||||
|
From your terminal, just type `go run main.go` from this
|
||||||
|
example's directory.
|
|
@ -0,0 +1,267 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
// See the MakeTabFrame() function just below for the meat of this example.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/render/event"
|
||||||
|
"git.kirsle.net/go/render/sdl"
|
||||||
|
"git.kirsle.net/go/ui"
|
||||||
|
"git.kirsle.net/go/ui/theme"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Program globals.
|
||||||
|
var (
|
||||||
|
// Size of the MainWindow.
|
||||||
|
Width = 1024
|
||||||
|
Height = 768
|
||||||
|
|
||||||
|
// Cascade offset for creating multiple windows.
|
||||||
|
Cascade = render.NewPoint(10, 32)
|
||||||
|
CascadeStep = render.NewPoint(24, 24)
|
||||||
|
CascadeLoops = 1
|
||||||
|
|
||||||
|
// Colors for each window created.
|
||||||
|
WindowColors = []render.Color{
|
||||||
|
render.Blue,
|
||||||
|
render.Red,
|
||||||
|
render.DarkYellow,
|
||||||
|
render.DarkGreen,
|
||||||
|
render.DarkCyan,
|
||||||
|
render.DarkBlue,
|
||||||
|
render.DarkRed,
|
||||||
|
}
|
||||||
|
WindowID int
|
||||||
|
OpenWindows int
|
||||||
|
|
||||||
|
TabFont = render.Text{
|
||||||
|
Size: 10,
|
||||||
|
Color: render.Black,
|
||||||
|
Padding: 4,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
sdl.DefaultFontFilename = "../DejaVuSans.ttf"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeTabFrame is the example use of the TabFrame widget.
|
||||||
|
// The rest of this file is basically a copy of the eg/windows
|
||||||
|
// demo, except each window embeds the TabFrame.
|
||||||
|
func MakeTabFrame(mw *ui.MainWindow) *ui.TabFrame {
|
||||||
|
notebook := ui.NewTabFrame("Example")
|
||||||
|
|
||||||
|
// AddTab gives you the Frame to populate for that tab.
|
||||||
|
|
||||||
|
// First Tab contents.
|
||||||
|
tab1 := notebook.AddTab("Tab 1", ui.NewLabel(ui.Label{
|
||||||
|
Text: "First Tab",
|
||||||
|
Font: TabFont,
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
label := ui.NewLabel(ui.Label{
|
||||||
|
Text: "Hello world",
|
||||||
|
Font: render.Text{
|
||||||
|
Size: 24,
|
||||||
|
Color: render.SkyBlue,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
tab1.Pack(label, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
})
|
||||||
|
|
||||||
|
label2 := ui.NewLabel(ui.Label{
|
||||||
|
Text: "This is the text content of the first\n" +
|
||||||
|
"of the three tab frames.",
|
||||||
|
Font: render.Text{
|
||||||
|
Size: 10,
|
||||||
|
Color: render.SkyBlue.Darken(40),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
tab1.Pack(label2, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
PadY: 8,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second Tab.
|
||||||
|
tab2 := notebook.AddTab("Tab 2", ui.NewLabel(ui.Label{
|
||||||
|
Text: "Second",
|
||||||
|
Font: TabFont,
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
label := ui.NewLabel(ui.Label{
|
||||||
|
Text: "Goodbye Mars",
|
||||||
|
Font: render.Text{
|
||||||
|
Size: 24,
|
||||||
|
Color: render.Orange,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
tab2.Pack(label, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
})
|
||||||
|
|
||||||
|
label2 := ui.NewLabel(ui.Label{
|
||||||
|
Text: "This is the text content of the second\n" +
|
||||||
|
"of the three tab frames.\n\nIt has longer text\nin it!",
|
||||||
|
Font: render.Text{
|
||||||
|
Size: 10,
|
||||||
|
Color: render.Orange.Darken(20),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
tab2.Pack(label2, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
PadY: 8,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third Tab.
|
||||||
|
tab3 := notebook.AddTab("Tab 3", ui.NewLabel(ui.Label{
|
||||||
|
Text: "Third",
|
||||||
|
Font: TabFont,
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
label := ui.NewLabel(ui.Label{
|
||||||
|
Text: "The Third Tab",
|
||||||
|
Font: render.Text{
|
||||||
|
Size: 24,
|
||||||
|
Color: render.Pink,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
tab3.Pack(label, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
})
|
||||||
|
|
||||||
|
label2 := ui.NewLabel(ui.Label{
|
||||||
|
Text: "This is the text content of the third\n" +
|
||||||
|
"of the tab frames.",
|
||||||
|
Font: render.Text{
|
||||||
|
Size: 10,
|
||||||
|
Color: render.Pink.Darken(40),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
tab3.Pack(label2, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
PadY: 8,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
notebook.Supervise(mw.Supervisor())
|
||||||
|
|
||||||
|
// notebook.SetBackground(render.DarkGrey)
|
||||||
|
return notebook
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
mw, err := ui.NewMainWindow("TabFrame Example", Width, Height)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark theme.
|
||||||
|
// ui.Theme = theme.DefaultDark
|
||||||
|
|
||||||
|
// Menu bar.
|
||||||
|
menu := ui.NewMenuBar("Main Menu")
|
||||||
|
file := menu.AddMenu("UI Theme")
|
||||||
|
file.AddItem("Default", func() {
|
||||||
|
ui.Theme = theme.Default
|
||||||
|
addWindow(mw)
|
||||||
|
})
|
||||||
|
file.AddItem("DefaultFlat", func() {
|
||||||
|
ui.Theme = theme.DefaultFlat
|
||||||
|
addWindow(mw)
|
||||||
|
})
|
||||||
|
file.AddItem("DefaultDark", func() {
|
||||||
|
ui.Theme = theme.DefaultDark
|
||||||
|
addWindow(mw)
|
||||||
|
})
|
||||||
|
file.AddSeparator()
|
||||||
|
file.AddItem("Close all windows", func() {
|
||||||
|
OpenWindows -= mw.Supervisor().CloseAllWindows()
|
||||||
|
})
|
||||||
|
|
||||||
|
menu.Supervise(mw.Supervisor())
|
||||||
|
menu.Compute(mw.Engine)
|
||||||
|
mw.Pack(menu, menu.PackTop())
|
||||||
|
|
||||||
|
// Add some windows to play with.
|
||||||
|
addWindow(mw)
|
||||||
|
addWindow(mw)
|
||||||
|
|
||||||
|
mw.SetBackground(render.White)
|
||||||
|
|
||||||
|
mw.OnLoop(func(e *event.State) {
|
||||||
|
if e.Escape {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mw.MainLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new child window.
|
||||||
|
func addWindow(mw *ui.MainWindow) {
|
||||||
|
var (
|
||||||
|
color = WindowColors[WindowID%len(WindowColors)]
|
||||||
|
title = fmt.Sprintf("Window %d (%s)", WindowID+1, ui.Theme.Name)
|
||||||
|
)
|
||||||
|
WindowID++
|
||||||
|
|
||||||
|
win1 := ui.NewWindow(title)
|
||||||
|
win1.SetButtons(ui.CloseButton)
|
||||||
|
win1.ActiveTitleBackground = color
|
||||||
|
win1.InactiveTitleBackground = color.Darken(60)
|
||||||
|
win1.InactiveTitleForeground = render.Grey
|
||||||
|
win1.Configure(ui.Config{
|
||||||
|
Width: 320,
|
||||||
|
Height: 240,
|
||||||
|
})
|
||||||
|
win1.Compute(mw.Engine)
|
||||||
|
win1.Supervise(mw.Supervisor())
|
||||||
|
|
||||||
|
// Re-open a window when the last one is closed.
|
||||||
|
OpenWindows++
|
||||||
|
win1.Handle(ui.CloseWindow, func(ed ui.EventData) error {
|
||||||
|
OpenWindows--
|
||||||
|
if OpenWindows <= 0 {
|
||||||
|
addWindow(mw)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Default placement via cascade.
|
||||||
|
win1.MoveTo(Cascade)
|
||||||
|
Cascade.Add(CascadeStep)
|
||||||
|
if Cascade.Y > Height-240-64 {
|
||||||
|
CascadeLoops++
|
||||||
|
Cascade.Y = 32
|
||||||
|
Cascade.X = 24 * CascadeLoops
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the TabFrame.
|
||||||
|
tabframe := MakeTabFrame(mw)
|
||||||
|
win1.Pack(tabframe, ui.Pack{
|
||||||
|
Side: ui.W,
|
||||||
|
Expand: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add a window duplicator button.
|
||||||
|
btn2 := ui.NewButton(title+":Button2", ui.NewLabel(ui.Label{
|
||||||
|
Text: "New Window",
|
||||||
|
}))
|
||||||
|
btn2.Handle(ui.Click, func(ed ui.EventData) error {
|
||||||
|
addWindow(mw)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
btn2.Compute(mw.Engine)
|
||||||
|
mw.Add(btn2)
|
||||||
|
win1.Compute(mw.Engine)
|
||||||
|
win1.Place(btn2, ui.Place{
|
||||||
|
Bottom: 12,
|
||||||
|
Right: 12,
|
||||||
|
})
|
||||||
|
}
|
After Width: | Height: | Size: 42 KiB |
|
@ -0,0 +1,11 @@
|
||||||
|
.PHONY: run
|
||||||
|
run:
|
||||||
|
go run main.go
|
||||||
|
|
||||||
|
.PHONY: wasm
|
||||||
|
wasm:
|
||||||
|
GOOS=js GOARCH=wasm go build -v -o windows.wasm main_wasm.go
|
||||||
|
|
||||||
|
.PHONY: wasm-serve
|
||||||
|
wasm-serve: wasm
|
||||||
|
../wasm-common/serve.sh
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Themes Example
|
||||||
|
|
||||||
|
![Screenshot](screenshot.png)
|
||||||
|
|
||||||
|
This demo shows off experimental UI theme support.
|
||||||
|
|
||||||
|
The main menu bar lets you open a Window with widgets all using a
|
||||||
|
selected theme.
|
||||||
|
|
||||||
|
## Running It
|
||||||
|
|
||||||
|
From your terminal, just type `go run main.go` or `make run` from this
|
||||||
|
example's directory.
|
|
@ -0,0 +1,107 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/render/event"
|
||||||
|
"git.kirsle.net/go/render/sdl"
|
||||||
|
"git.kirsle.net/go/ui"
|
||||||
|
"git.kirsle.net/go/ui/theme"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Program globals.
|
||||||
|
var (
|
||||||
|
// Size of the MainWindow.
|
||||||
|
Width = 1024
|
||||||
|
Height = 768
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
sdl.DefaultFontFilename = "../DejaVuSans.ttf"
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
mw, err := ui.NewMainWindow("Theme Demo", Width, Height)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu bar.
|
||||||
|
menu := ui.NewMenuBar("Main Menu")
|
||||||
|
file := menu.AddMenu("Select Theme")
|
||||||
|
file.AddItem("Default", func() {
|
||||||
|
addWindow(mw, theme.Default)
|
||||||
|
})
|
||||||
|
file.AddItem("DefaultFlat", func() {
|
||||||
|
addWindow(mw, theme.DefaultFlat)
|
||||||
|
})
|
||||||
|
file.AddItem("DefaultDark", func() {
|
||||||
|
addWindow(mw, theme.DefaultDark)
|
||||||
|
})
|
||||||
|
|
||||||
|
menu.Supervise(mw.Supervisor())
|
||||||
|
menu.Compute(mw.Engine)
|
||||||
|
mw.Pack(menu, menu.PackTop())
|
||||||
|
|
||||||
|
mw.SetBackground(render.White)
|
||||||
|
|
||||||
|
mw.OnLoop(func(e *event.State) {
|
||||||
|
if e.Escape {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mw.MainLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new child window.
|
||||||
|
func addWindow(mw *ui.MainWindow, theme theme.Theme) {
|
||||||
|
ui.Theme = theme
|
||||||
|
|
||||||
|
win1 := ui.NewWindow(theme.Name)
|
||||||
|
win1.SetButtons(ui.CloseButton)
|
||||||
|
win1.Configure(ui.Config{
|
||||||
|
Width: 320,
|
||||||
|
Height: 240,
|
||||||
|
})
|
||||||
|
win1.Compute(mw.Engine)
|
||||||
|
win1.Supervise(mw.Supervisor())
|
||||||
|
|
||||||
|
// Draw a label.
|
||||||
|
label := ui.NewLabel(ui.Label{
|
||||||
|
Text: theme.Name,
|
||||||
|
})
|
||||||
|
win1.Place(label, ui.Place{
|
||||||
|
Top: 10,
|
||||||
|
Left: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add a button with tooltip.
|
||||||
|
btn2 := ui.NewButton(theme.Name+":Button2", ui.NewLabel(ui.Label{
|
||||||
|
Text: "Button",
|
||||||
|
}))
|
||||||
|
btn2.Handle(ui.Click, func(ed ui.EventData) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
mw.Add(btn2)
|
||||||
|
win1.Place(btn2, ui.Place{
|
||||||
|
Top: 10,
|
||||||
|
Right: 10,
|
||||||
|
})
|
||||||
|
ui.NewTooltip(btn2, ui.Tooltip{
|
||||||
|
Text: "Hello world!",
|
||||||
|
Edge: ui.Bottom,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add a checkbox.
|
||||||
|
var b bool
|
||||||
|
cb := ui.NewCheckbox("Checkbox", &b, ui.NewLabel(ui.Label{
|
||||||
|
Text: "Check me!",
|
||||||
|
}))
|
||||||
|
mw.Add(cb)
|
||||||
|
win1.Place(cb, ui.Place{
|
||||||
|
Top: 30,
|
||||||
|
Left: 10,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,169 @@
|
||||||
|
// +build js,wasm
|
||||||
|
|
||||||
|
// WebAssembly version of the window manager demo.
|
||||||
|
// To build: make wasm
|
||||||
|
// To test: make wasm-serve
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/render/canvas"
|
||||||
|
"git.kirsle.net/go/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Program globals.
|
||||||
|
var (
|
||||||
|
ThrottleFPS = 1000 / 60
|
||||||
|
|
||||||
|
// Size of the MainWindow.
|
||||||
|
Width = 1024
|
||||||
|
Height = 768
|
||||||
|
|
||||||
|
// Cascade offset for creating multiple windows.
|
||||||
|
Cascade = render.NewPoint(10, 32)
|
||||||
|
CascadeStep = render.NewPoint(24, 24)
|
||||||
|
CascadeLoops = 1
|
||||||
|
|
||||||
|
// Colors for each window created.
|
||||||
|
WindowColors = []render.Color{
|
||||||
|
render.Blue,
|
||||||
|
render.Red,
|
||||||
|
render.DarkYellow,
|
||||||
|
render.DarkGreen,
|
||||||
|
render.DarkCyan,
|
||||||
|
render.DarkBlue,
|
||||||
|
render.DarkRed,
|
||||||
|
}
|
||||||
|
WindowID int
|
||||||
|
OpenWindows int
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
mw, err := canvas.New("canvas")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind DOM event handlers.
|
||||||
|
mw.AddEventListeners()
|
||||||
|
|
||||||
|
supervisor := ui.NewSupervisor()
|
||||||
|
|
||||||
|
frame := ui.NewFrame("Main Frame")
|
||||||
|
frame.Resize(render.NewRect(mw.WindowSize()))
|
||||||
|
frame.Compute(mw)
|
||||||
|
|
||||||
|
_, height := mw.WindowSize()
|
||||||
|
lbl := ui.NewLabel(ui.Label{
|
||||||
|
Text: "Window Manager Demo",
|
||||||
|
Font: render.Text{
|
||||||
|
FontFilename: "DejaVuSans.ttf",
|
||||||
|
Size: 32,
|
||||||
|
Color: render.SkyBlue,
|
||||||
|
Shadow: render.SkyBlue.Darken(60),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
lbl.Compute(mw)
|
||||||
|
lbl.MoveTo(render.NewPoint(
|
||||||
|
20,
|
||||||
|
height-lbl.Size().H-20,
|
||||||
|
))
|
||||||
|
|
||||||
|
// Menu bar.
|
||||||
|
menu := ui.NewMenuBar("Main Menu")
|
||||||
|
file := menu.AddMenu("Options")
|
||||||
|
file.AddItem("New window", func() {
|
||||||
|
addWindow(mw, frame, supervisor)
|
||||||
|
})
|
||||||
|
file.AddItem("Close all windows", func() {
|
||||||
|
OpenWindows -= supervisor.CloseAllWindows()
|
||||||
|
})
|
||||||
|
|
||||||
|
menu.Supervise(supervisor)
|
||||||
|
menu.Compute(mw)
|
||||||
|
frame.Pack(menu, menu.PackTop())
|
||||||
|
|
||||||
|
// Add some windows to play with.
|
||||||
|
addWindow(mw, frame, supervisor)
|
||||||
|
addWindow(mw, frame, supervisor)
|
||||||
|
|
||||||
|
for {
|
||||||
|
mw.Clear(render.RGBA(255, 255, 200, 255))
|
||||||
|
start := time.Now()
|
||||||
|
ev, err := mw.Poll()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.Present(mw, frame.Point())
|
||||||
|
lbl.Present(mw, lbl.Point())
|
||||||
|
supervisor.Loop(ev)
|
||||||
|
supervisor.Present(mw)
|
||||||
|
|
||||||
|
var delay uint32
|
||||||
|
elapsed := time.Now().Sub(start)
|
||||||
|
tmp := elapsed / time.Millisecond
|
||||||
|
if ThrottleFPS-int(tmp) > 0 {
|
||||||
|
delay = uint32(ThrottleFPS - int(tmp))
|
||||||
|
}
|
||||||
|
mw.Delay(delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new child window.
|
||||||
|
func addWindow(engine render.Engine, parent *ui.Frame, sup *ui.Supervisor) {
|
||||||
|
var (
|
||||||
|
color = WindowColors[WindowID%len(WindowColors)]
|
||||||
|
title = fmt.Sprintf("Window %d", WindowID+1)
|
||||||
|
)
|
||||||
|
WindowID++
|
||||||
|
|
||||||
|
win1 := ui.NewWindow(title)
|
||||||
|
win1.SetButtons(ui.CloseButton)
|
||||||
|
win1.ActiveTitleBackground = color
|
||||||
|
win1.InactiveTitleBackground = color.Darken(60)
|
||||||
|
win1.InactiveTitleForeground = render.Grey
|
||||||
|
win1.Configure(ui.Config{
|
||||||
|
Width: 320,
|
||||||
|
Height: 240,
|
||||||
|
})
|
||||||
|
win1.Compute(engine)
|
||||||
|
win1.Supervise(sup)
|
||||||
|
|
||||||
|
// Re-open a window when the last one is closed.
|
||||||
|
OpenWindows++
|
||||||
|
win1.Handle(ui.CloseWindow, func(ed ui.EventData) error {
|
||||||
|
OpenWindows--
|
||||||
|
if OpenWindows <= 0 {
|
||||||
|
addWindow(engine, parent, sup)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Default placement via cascade.
|
||||||
|
win1.MoveTo(Cascade)
|
||||||
|
Cascade.Add(CascadeStep)
|
||||||
|
if Cascade.Y > Height-240-64 {
|
||||||
|
CascadeLoops++
|
||||||
|
Cascade.Y = 24
|
||||||
|
Cascade.X = 24 * CascadeLoops
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a window duplicator button.
|
||||||
|
btn2 := ui.NewButton(title+":Button2", ui.NewLabel(ui.Label{
|
||||||
|
Text: "New Window",
|
||||||
|
}))
|
||||||
|
btn2.Handle(ui.Click, func(ed ui.EventData) error {
|
||||||
|
addWindow(engine, parent, sup)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
sup.Add(btn2)
|
||||||
|
win1.Place(btn2, ui.Place{
|
||||||
|
Top: 10,
|
||||||
|
Right: 10,
|
||||||
|
})
|
||||||
|
}
|
After Width: | Height: | Size: 16 KiB |
|
@ -0,0 +1,10 @@
|
||||||
|
# Tooltips Demo
|
||||||
|
|
||||||
|
![Screenshot](screenshot.png)
|
||||||
|
|
||||||
|
A screen full of buttons having different Tooltip properties.
|
||||||
|
|
||||||
|
## Running It
|
||||||
|
|
||||||
|
From your terminal, just type `go run main.go` from this
|
||||||
|
example's directory.
|
|
@ -132,8 +132,9 @@ func CreateButtons(window *ui.MainWindow, parent *ui.Frame) {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// When clicked, change the window title to ID this button.
|
// When clicked, change the window title to ID this button.
|
||||||
button.Handle(ui.Click, func(ed ui.EventData) {
|
button.Handle(ui.Click, func(ed ui.EventData) error {
|
||||||
window.SetTitle(parent.Name + ": " + setting.Label)
|
window.SetTitle(parent.Name + ": " + setting.Label)
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// Tooltip for it.
|
// Tooltip for it.
|
||||||
|
|
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,11 @@
|
||||||
|
# Windows Demo
|
||||||
|
|
||||||
|
![Screenshot](screenshot.png)
|
||||||
|
|
||||||
|
This demo lets you open a bunch of Window widgets that can be moved
|
||||||
|
around, overlapped, and closed.
|
||||||
|
|
||||||
|
## Running It
|
||||||
|
|
||||||
|
From your terminal, just type `go run main.go` from this
|
||||||
|
example's directory.
|
After Width: | Height: | Size: 16 KiB |
13
frame.go
|
@ -13,8 +13,8 @@ type Frame struct {
|
||||||
BaseWidget
|
BaseWidget
|
||||||
|
|
||||||
// Widget placement settings.
|
// Widget placement settings.
|
||||||
packs map[Side][]packedWidget // Packed widgets
|
packs map[Side][]*packedWidget // Packed widgets
|
||||||
placed []placedWidget // Placed widgets
|
placed []*placedWidget // Placed widgets
|
||||||
widgets []Widget
|
widgets []Widget
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ type Frame struct {
|
||||||
func NewFrame(name string) *Frame {
|
func NewFrame(name string) *Frame {
|
||||||
w := &Frame{
|
w := &Frame{
|
||||||
Name: name,
|
Name: name,
|
||||||
packs: map[Side][]packedWidget{},
|
packs: map[Side][]*packedWidget{},
|
||||||
widgets: []Widget{},
|
widgets: []Widget{},
|
||||||
}
|
}
|
||||||
w.SetBackground(render.RGBA(1, 0, 0, 0)) // invisible default BG
|
w.SetBackground(render.RGBA(1, 0, 0, 0)) // invisible default BG
|
||||||
|
@ -37,7 +37,7 @@ func NewFrame(name string) *Frame {
|
||||||
// Setup ensures all the Frame's data is initialized and not null.
|
// Setup ensures all the Frame's data is initialized and not null.
|
||||||
func (w *Frame) Setup() {
|
func (w *Frame) Setup() {
|
||||||
if w.packs == nil {
|
if w.packs == nil {
|
||||||
w.packs = map[Side][]packedWidget{}
|
w.packs = map[Side][]*packedWidget{}
|
||||||
}
|
}
|
||||||
if w.widgets == nil {
|
if w.widgets == nil {
|
||||||
w.widgets = []Widget{}
|
w.widgets = []Widget{}
|
||||||
|
@ -99,7 +99,10 @@ func (w *Frame) Present(e render.Engine, P render.Point) {
|
||||||
|
|
||||||
// Draw the widgets.
|
// Draw the widgets.
|
||||||
for _, child := range w.widgets {
|
for _, child := range w.widgets {
|
||||||
// child.Compute(e)
|
if child.Hidden() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
p := child.Point()
|
p := child.Point()
|
||||||
moveTo := render.NewPoint(
|
moveTo := render.NewPoint(
|
||||||
P.X+p.X+w.BoxThickness(1),
|
P.X+p.X+w.BoxThickness(1),
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,9 +31,17 @@ func (w *Frame) Pack(child Widget, config ...Pack) {
|
||||||
C = config[0]
|
C = config[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update an already placed widget.
|
||||||
|
for _, current := range w.packs[C.Side] {
|
||||||
|
if current.widget == child {
|
||||||
|
current.pack = C
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the pack list for this side?
|
// Initialize the pack list for this side?
|
||||||
if _, ok := w.packs[C.Side]; !ok {
|
if _, ok := w.packs[C.Side]; !ok {
|
||||||
w.packs[C.Side] = []packedWidget{}
|
w.packs[C.Side] = []*packedWidget{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Padding: if the user only provided Padding add it to both
|
// Padding: if the user only provided Padding add it to both
|
||||||
|
@ -49,13 +59,41 @@ func (w *Frame) Pack(child Widget, config ...Pack) {
|
||||||
// Adopt the child widget so it can access the Frame.
|
// Adopt the child widget so it can access the Frame.
|
||||||
child.SetParent(w)
|
child.SetParent(w)
|
||||||
|
|
||||||
w.packs[C.Side] = append(w.packs[C.Side], packedWidget{
|
w.packs[C.Side] = append(w.packs[C.Side], &packedWidget{
|
||||||
widget: child,
|
widget: child,
|
||||||
pack: C,
|
pack: C,
|
||||||
})
|
})
|
||||||
w.Add(child)
|
w.Add(child)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unpack removes the widget from the packed lists.
|
||||||
|
func (w *Frame) Unpack(child Widget) bool {
|
||||||
|
var any = false
|
||||||
|
for side, widgets := range w.packs {
|
||||||
|
var (
|
||||||
|
replace = []*packedWidget{}
|
||||||
|
found = false
|
||||||
|
)
|
||||||
|
|
||||||
|
fmt.Printf("unpack:%s side:%s\n", child, side)
|
||||||
|
|
||||||
|
for _, widget := range widgets {
|
||||||
|
if widget.widget == child {
|
||||||
|
fmt.Printf("found!\n")
|
||||||
|
found = true
|
||||||
|
any = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
replace = append(replace, widget)
|
||||||
|
}
|
||||||
|
|
||||||
|
if found {
|
||||||
|
w.packs[side] = replace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return any
|
||||||
|
}
|
||||||
|
|
||||||
// computePacked processes all the Pack layout widgets in the Frame.
|
// computePacked processes all the Pack layout widgets in the Frame.
|
||||||
func (w *Frame) computePacked(e render.Engine) {
|
func (w *Frame) computePacked(e render.Engine) {
|
||||||
var (
|
var (
|
||||||
|
@ -68,8 +106,8 @@ func (w *Frame) computePacked(e render.Engine) {
|
||||||
// so we can expand them to fill remaining space in fixed size Frames.
|
// so we can expand them to fill remaining space in fixed size Frames.
|
||||||
maxWidth int
|
maxWidth int
|
||||||
maxHeight int
|
maxHeight int
|
||||||
visited = []packedWidget{}
|
visited = []*packedWidget{}
|
||||||
expanded = []packedWidget{}
|
expanded = []*packedWidget{}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Iterate through all directions and compute how much space to
|
// Iterate through all directions and compute how much space to
|
||||||
|
|
|
@ -37,9 +37,18 @@ type placedWidget struct {
|
||||||
place Place
|
place Place
|
||||||
}
|
}
|
||||||
|
|
||||||
// Place a widget into the frame.
|
// Place a widget into the frame. You may call Place on a widget multiple times to update its configuration.
|
||||||
func (w *Frame) Place(child Widget, config Place) {
|
func (w *Frame) Place(child Widget, config Place) {
|
||||||
w.placed = append(w.placed, placedWidget{
|
// Update an already placed widget.
|
||||||
|
for _, current := range w.placed {
|
||||||
|
if current.widget == child {
|
||||||
|
current.place = config
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append it.
|
||||||
|
w.placed = append(w.placed, &placedWidget{
|
||||||
widget: child,
|
widget: child,
|
||||||
place: config,
|
place: config,
|
||||||
})
|
})
|
||||||
|
@ -54,6 +63,8 @@ func (w *Frame) Place(child Widget, config Place) {
|
||||||
func (w *Frame) computePlaced(e render.Engine) {
|
func (w *Frame) computePlaced(e render.Engine) {
|
||||||
var (
|
var (
|
||||||
frameSize = w.BoxSize()
|
frameSize = w.BoxSize()
|
||||||
|
// maxWidth int
|
||||||
|
// maxHeight int
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, row := range w.placed {
|
for _, row := range w.placed {
|
||||||
|
@ -87,6 +98,7 @@ func (w *Frame) computePlaced(e render.Engine) {
|
||||||
if row.place.Middle {
|
if row.place.Middle {
|
||||||
moveTo.Y = frameSize.H - (w.Size().H / 2) - (row.widget.Size().H / 2)
|
moveTo.Y = frameSize.H - (w.Size().H / 2) - (row.widget.Size().H / 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
row.widget.MoveTo(moveTo)
|
row.widget.MoveTo(moveTo)
|
||||||
row.widget.Compute(e)
|
row.widget.Compute(e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ func AbsolutePosition(w Widget) render.Point {
|
||||||
var (
|
var (
|
||||||
node = w
|
node = w
|
||||||
ok bool
|
ok bool
|
||||||
|
pt render.Point
|
||||||
)
|
)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
@ -20,7 +21,9 @@ func AbsolutePosition(w Widget) render.Point {
|
||||||
return abs
|
return abs
|
||||||
}
|
}
|
||||||
|
|
||||||
abs.Add(node.Point())
|
pt = node.Point()
|
||||||
|
pt.Add(render.NewPoint(node.BorderSize(), node.BorderSize()))
|
||||||
|
abs.Add(pt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
/*
|
||||||
|
Glyph images as Base64 encoded PNGs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// List of available glyphs.
|
||||||
|
const (
|
||||||
|
// Downward pointed black arrow 9x9 pixels.
|
||||||
|
GlyphDownArrow9x9 = `iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI
|
||||||
|
WXMAAC4jAAAuIwF4pT92AAAAKklEQVQY02NgoBZgZGBg+E+MIgYCChkZkTj/cRnCiCb4H4stWMF/
|
||||||
|
BpoBAGQSBQOpAugRAAAAAElFTkSuQmCC`
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetGlyph loads a PNG image from a hard-coded glyph.
|
||||||
|
func GetGlyph(b64 string) (image.Image, error) {
|
||||||
|
data, err := base64.StdEncoding.DecodeString(b64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bytes.NewReader(data)
|
||||||
|
return png.Decode(scanner)
|
||||||
|
}
|
10
go.mod
|
@ -1,7 +1,9 @@
|
||||||
module git.kirsle.net/go/ui
|
module git.kirsle.net/go/ui
|
||||||
|
|
||||||
go 1.13
|
go 1.16
|
||||||
|
|
||||||
replace git.kirsle.net/go/render => /home/kirsle/go/src/git.kirsle.net/go/render
|
require (
|
||||||
|
git.kirsle.net/go/render v0.0.0-20220505053906-129a24300dfa
|
||||||
require git.kirsle.net/go/render v0.0.0-20200102014411-4d008b5c468d
|
github.com/veandco/go-sdl2 v0.4.36 // indirect
|
||||||
|
golang.org/x/image v0.14.0
|
||||||
|
)
|
||||||
|
|
40
go.sum
|
@ -1,7 +1,39 @@
|
||||||
git.kirsle.net/go/render v0.0.0-20200102014411-4d008b5c468d h1:vErak6oVRT2dosyQzcwkjXyWQ2NRIVL8q9R8NOUTtsg=
|
git.kirsle.net/go/render v0.0.0-20220505053906-129a24300dfa h1:Oa99SXkmFGnUNy+toPMQyW/eYotN1nZ9BWAThQ/huiM=
|
||||||
git.kirsle.net/go/render v0.0.0-20200102014411-4d008b5c468d/go.mod h1:ywZtC+zE2SpeObfkw0OvG01pWHQadsVQ4WDKOYzaejc=
|
git.kirsle.net/go/render v0.0.0-20220505053906-129a24300dfa/go.mod h1:ss7pvZbGWrMaDuZwyUTjV9+T0AJwAkxZZHwMFsvHrkk=
|
||||||
github.com/veandco/go-sdl2 v0.4.1 h1:HmSBvVmKWI8LAOeCfTTM8R33rMyPcs6U3o8n325c9Qg=
|
|
||||||
github.com/veandco/go-sdl2 v0.4.1/go.mod h1:FB+kTpX9YTE+urhYiClnRzpOXbiWgaU3+5F2AB78DPg=
|
github.com/veandco/go-sdl2 v0.4.1/go.mod h1:FB+kTpX9YTE+urhYiClnRzpOXbiWgaU3+5F2AB78DPg=
|
||||||
golang.org/x/image v0.0.0-20200119044424-58c23975cae1 h1:5h3ngYt7+vXCDZCup/HkCQgW5XwmSvR/nA2JmJ0RErg=
|
github.com/veandco/go-sdl2 v0.4.36 h1:Ltydev536rRQodmIrTWFZ3dRp5A+/6t5CYvbi4Kvia0=
|
||||||
|
github.com/veandco/go-sdl2 v0.4.36/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
|
||||||
|
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
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=
|
||||||
|
|
109
image.go
|
@ -4,11 +4,13 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
|
"golang.org/x/image/bmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ImageType for supported image formats.
|
// ImageType for supported image formats.
|
||||||
|
@ -27,8 +29,8 @@ type Image struct {
|
||||||
|
|
||||||
// Configurable fields for the constructor.
|
// Configurable fields for the constructor.
|
||||||
Type ImageType
|
Type ImageType
|
||||||
Image image.Image
|
Image image.Image // a Go image version
|
||||||
texture render.Texturer
|
texture render.Texturer // (SDL2) Texture, lazy inited on Present.
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewImage creates a new Image.
|
// NewImage creates a new Image.
|
||||||
|
@ -46,15 +48,24 @@ func NewImage(c Image) *Image {
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImageFromImage creates an Image from a Go standard library image.Image.
|
||||||
|
func ImageFromImage(im image.Image) (*Image, error) {
|
||||||
|
return &Image{
|
||||||
|
Type: PNG,
|
||||||
|
Image: im,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ImageFromTexture creates an Image from a texture.
|
// ImageFromTexture creates an Image from a texture.
|
||||||
func ImageFromTexture(tex render.Texturer) *Image {
|
func ImageFromTexture(tex render.Texturer) *Image {
|
||||||
return &Image{
|
return &Image{
|
||||||
texture: tex,
|
texture: tex,
|
||||||
|
Image: tex.Image(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageFromFile creates an Image by opening a file from disk.
|
// ImageFromFile creates an Image by opening a file from disk.
|
||||||
func ImageFromFile(e render.Engine, filename string) (*Image, error) {
|
func ImageFromFile(filename string) (*Image, error) {
|
||||||
fh, err := os.Open(filename)
|
fh, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -65,17 +76,24 @@ func ImageFromFile(e render.Engine, filename string) (*Image, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tex, err := e.StoreTexture(filename, img)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Image{
|
return &Image{
|
||||||
Image: img,
|
Image: img,
|
||||||
texture: tex,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReplaceFromImage replaces the image with a new image.
|
||||||
|
func (w *Image) ReplaceFromImage(im image.Image) error {
|
||||||
|
// Free the old texture.
|
||||||
|
if w.texture != nil {
|
||||||
|
if err := w.texture.Free(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.texture = nil
|
||||||
|
}
|
||||||
|
w.Image = im
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// OpenImage initializes an Image with a given file name.
|
// OpenImage initializes an Image with a given file name.
|
||||||
//
|
//
|
||||||
// The file extension is important and should be a supported ImageType.
|
// The file extension is important and should be a supported ImageType.
|
||||||
|
@ -94,12 +112,34 @@ func OpenImage(e render.Engine, filename string) (*Image, error) {
|
||||||
return nil, fmt.Errorf("OpenImage: %s: not a supported image type", filename)
|
return nil, fmt.Errorf("OpenImage: %s: not a supported image type", filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
tex, err := e.LoadTexture(filename)
|
// Open the file from disk.
|
||||||
|
fh, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
w.texture = tex
|
// Parse it.
|
||||||
|
switch w.Type {
|
||||||
|
case PNG:
|
||||||
|
img, err := png.Decode(fh)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.Image = img
|
||||||
|
case JPEG:
|
||||||
|
img, err := jpeg.Decode(fh)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.Image = img
|
||||||
|
case BMP:
|
||||||
|
img, err := bmp.Decode(fh)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.Image = img
|
||||||
|
}
|
||||||
|
|
||||||
return w, nil
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,16 +156,39 @@ func (w *Image) GetRGBA() *image.RGBA {
|
||||||
return rgba
|
return rgba
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute the widget.
|
// Size returns the dimensions of the image which is also the widget's size.
|
||||||
func (w *Image) Compute(e render.Engine) {
|
func (w *Image) Size() render.Rect {
|
||||||
w.Resize(w.texture.Size())
|
if w.Image != nil {
|
||||||
|
bounds := w.Image.Bounds().Canon()
|
||||||
// Call the BaseWidget Compute in case we have subscribers.
|
return render.Rect{
|
||||||
w.BaseWidget.Compute(e)
|
W: bounds.Max.X,
|
||||||
|
H: bounds.Max.Y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return w.BaseWidget.Size()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present the widget.
|
// Counter for unique SDL2 texture names.
|
||||||
|
var __imageID int
|
||||||
|
|
||||||
|
// Present the widget. This should be called on your main thread
|
||||||
|
// if using SDL2 in case it needs to generate textures.
|
||||||
func (w *Image) Present(e render.Engine, p render.Point) {
|
func (w *Image) Present(e render.Engine, p render.Point) {
|
||||||
|
// Lazy load the (e.g. SDL2) texture from the stored bitmap.
|
||||||
|
if w.texture == nil {
|
||||||
|
if w.Image == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
__imageID++
|
||||||
|
tex, err := e.StoreTexture(fmt.Sprintf("ui.Image(%d).png", __imageID), w.Image)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("ui.Image.Present(): could not make texture: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.texture = tex
|
||||||
|
}
|
||||||
|
|
||||||
size := w.texture.Size()
|
size := w.texture.Size()
|
||||||
dst := render.Rect{
|
dst := render.Rect{
|
||||||
X: p.X,
|
X: p.X,
|
||||||
|
@ -138,3 +201,11 @@ func (w *Image) Present(e render.Engine, p render.Point) {
|
||||||
// Call the BaseWidget Present in case we have subscribers.
|
// Call the BaseWidget Present in case we have subscribers.
|
||||||
w.BaseWidget.Present(e, p)
|
w.BaseWidget.Present(e, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Destroy cleans up the image and releases textures.
|
||||||
|
func (w *Image) Destroy() {
|
||||||
|
if w.texture != nil {
|
||||||
|
w.texture.Free()
|
||||||
|
w.texture = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
14
label.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/ui/style"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultFont is the default font settings used for a Label.
|
// DefaultFont is the default font settings used for a Label.
|
||||||
|
@ -23,6 +24,7 @@ type Label struct {
|
||||||
IntVariable *int
|
IntVariable *int
|
||||||
Font render.Text
|
Font render.Text
|
||||||
|
|
||||||
|
style *style.Label
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
lineHeight int
|
lineHeight int
|
||||||
|
@ -36,6 +38,7 @@ func NewLabel(c Label) *Label {
|
||||||
IntVariable: c.IntVariable,
|
IntVariable: c.IntVariable,
|
||||||
Font: DefaultFont,
|
Font: DefaultFont,
|
||||||
}
|
}
|
||||||
|
w.SetStyle(Theme.Label)
|
||||||
if !c.Font.IsZero() {
|
if !c.Font.IsZero() {
|
||||||
w.Font = c.Font
|
w.Font = c.Font
|
||||||
}
|
}
|
||||||
|
@ -45,6 +48,17 @@ func NewLabel(c Label) *Label {
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetStyle sets the label's default style.
|
||||||
|
func (w *Label) SetStyle(v *style.Label) {
|
||||||
|
if v == nil {
|
||||||
|
v = &style.DefaultLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
w.style = v
|
||||||
|
w.SetBackground(w.style.Background)
|
||||||
|
w.Font.Color = w.style.Foreground
|
||||||
|
}
|
||||||
|
|
||||||
// text returns the label's displayed text, coming from the TextVariable if
|
// text returns the label's displayed text, coming from the TextVariable if
|
||||||
// available or else the Text attribute instead.
|
// available or else the Text attribute instead.
|
||||||
func (w *Label) text() render.Text {
|
func (w *Label) text() render.Text {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -88,7 +88,7 @@ func (w *MenuButton) Compute(e render.Engine) {
|
||||||
func (w *MenuButton) positionMenu(e render.Engine) {
|
func (w *MenuButton) positionMenu(e render.Engine) {
|
||||||
var (
|
var (
|
||||||
// Position and size of the MenuButton button.
|
// Position and size of the MenuButton button.
|
||||||
buttonPoint = w.Point()
|
buttonPoint = AbsolutePosition(w)
|
||||||
buttonSize = w.Size()
|
buttonSize = w.Size()
|
||||||
|
|
||||||
// Size of the actual desktop window.
|
// Size of the actual desktop window.
|
||||||
|
|
|
@ -0,0 +1,229 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pager is a frame with Pagers for paginated UI.
|
||||||
|
type Pager struct {
|
||||||
|
BaseWidget
|
||||||
|
|
||||||
|
// Config settings. NOTE: these are copied in the constructor,
|
||||||
|
// be sure to update it there too if you add a new option!
|
||||||
|
Name string // totally optional name
|
||||||
|
Page int // default 1
|
||||||
|
Pages int
|
||||||
|
PerPage int // default 20
|
||||||
|
MaxPageButtons int // max no. of individual pages to show, 0 = no limit
|
||||||
|
Font render.Text
|
||||||
|
OnChange func(page, perPage int)
|
||||||
|
|
||||||
|
supervisor *Supervisor
|
||||||
|
child Widget
|
||||||
|
buttons []Widget
|
||||||
|
page string // radio button value of Page
|
||||||
|
|
||||||
|
// Private options.
|
||||||
|
hovering bool
|
||||||
|
clicked bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPager creates a new Pager.
|
||||||
|
func NewPager(config Pager) *Pager {
|
||||||
|
w := &Pager{
|
||||||
|
Page: config.Page,
|
||||||
|
Pages: config.Pages,
|
||||||
|
PerPage: config.PerPage,
|
||||||
|
MaxPageButtons: config.MaxPageButtons,
|
||||||
|
Font: config.Font,
|
||||||
|
OnChange: config.OnChange,
|
||||||
|
buttons: []Widget{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// default settings
|
||||||
|
if w.Page == 0 {
|
||||||
|
w.Page = 1
|
||||||
|
}
|
||||||
|
if w.PerPage == 0 {
|
||||||
|
w.PerPage = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
w.IDFunc(func() string {
|
||||||
|
return fmt.Sprintf("Pager<%d of %d>", w.Page, w.PerPage)
|
||||||
|
})
|
||||||
|
|
||||||
|
w.child = w.setup()
|
||||||
|
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supervise the pager to make its buttons work.
|
||||||
|
func (w *Pager) Supervise(s *Supervisor) {
|
||||||
|
w.supervisor = s
|
||||||
|
for _, btn := range w.buttons {
|
||||||
|
w.supervisor.Add(btn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup the frame
|
||||||
|
func (w *Pager) setup() *Frame {
|
||||||
|
frame := NewFrame("Pager Frame")
|
||||||
|
frame.SetParent(w)
|
||||||
|
|
||||||
|
if w.Pages == 0 {
|
||||||
|
return frame
|
||||||
|
}
|
||||||
|
|
||||||
|
w.buttons = []Widget{}
|
||||||
|
w.page = fmt.Sprintf("%d", w.Page)
|
||||||
|
|
||||||
|
// Previous Page Button
|
||||||
|
prev := NewButton("Previous", NewLabel(Label{
|
||||||
|
Text: "<",
|
||||||
|
Font: w.Font,
|
||||||
|
}))
|
||||||
|
w.buttons = append(w.buttons, prev)
|
||||||
|
prev.Handle(Click, func(ed EventData) error {
|
||||||
|
return w.next(-1)
|
||||||
|
})
|
||||||
|
frame.Pack(prev, Pack{
|
||||||
|
Side: W,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Draw the numbered buttons.
|
||||||
|
for i := 1; i <= w.Pages; i++ {
|
||||||
|
page := fmt.Sprintf("%d", i)
|
||||||
|
|
||||||
|
if w.MaxPageButtons > 0 {
|
||||||
|
if i > w.MaxPageButtons {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// The final button shown; make this one always reflect the
|
||||||
|
// current page IF the current page is greater than this one.
|
||||||
|
if w.Page >= i && w.Page != i {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btn := NewRadioButton(
|
||||||
|
"Page "+page,
|
||||||
|
&w.page,
|
||||||
|
page,
|
||||||
|
NewLabel(Label{
|
||||||
|
Text: page,
|
||||||
|
Font: w.Font,
|
||||||
|
}))
|
||||||
|
w.buttons = append(w.buttons, btn)
|
||||||
|
|
||||||
|
btn.Handle(Click, func(ed EventData) error {
|
||||||
|
if w.OnChange != nil {
|
||||||
|
page, _ := strconv.Atoi(w.page)
|
||||||
|
w.OnChange(page, w.PerPage)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if w.supervisor != nil {
|
||||||
|
w.supervisor.Add(btn)
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.Pack(btn, Pack{
|
||||||
|
Side: W,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next Page Button
|
||||||
|
next := NewButton("Next", NewLabel(Label{
|
||||||
|
Text: ">",
|
||||||
|
Font: w.Font,
|
||||||
|
}))
|
||||||
|
w.buttons = append(w.buttons, next)
|
||||||
|
next.Handle(Click, func(ed EventData) error {
|
||||||
|
return w.next(1)
|
||||||
|
})
|
||||||
|
frame.Pack(next, Pack{
|
||||||
|
Side: W,
|
||||||
|
})
|
||||||
|
|
||||||
|
return frame
|
||||||
|
}
|
||||||
|
|
||||||
|
// next (1) or previous (-1) button
|
||||||
|
func (w *Pager) next(value int) error {
|
||||||
|
fmt.Printf("next(%d)\n", value)
|
||||||
|
intvalue, _ := strconv.Atoi(w.page)
|
||||||
|
intvalue += value
|
||||||
|
|
||||||
|
if intvalue < 1 {
|
||||||
|
intvalue = 1
|
||||||
|
} else if intvalue > w.Pages {
|
||||||
|
intvalue = w.Pages
|
||||||
|
}
|
||||||
|
|
||||||
|
w.page = fmt.Sprintf("%d", intvalue)
|
||||||
|
|
||||||
|
if w.OnChange != nil {
|
||||||
|
w.OnChange(intvalue, w.PerPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the size of the Pager.
|
||||||
|
func (w *Pager) Compute(e render.Engine) {
|
||||||
|
// Compute the size of the inner widget first.
|
||||||
|
w.child.Compute(e)
|
||||||
|
|
||||||
|
// Auto-resize only if we haven't been given a fixed size.
|
||||||
|
if !w.FixedSize() {
|
||||||
|
size := w.child.Size()
|
||||||
|
w.Resize(render.Rect{
|
||||||
|
W: size.W + w.BoxThickness(2),
|
||||||
|
H: size.H + w.BoxThickness(2),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
w.BaseWidget.Compute(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present the Pager.
|
||||||
|
func (w *Pager) Present(e render.Engine, P render.Point) {
|
||||||
|
if w.Hidden() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Compute(e)
|
||||||
|
var (
|
||||||
|
S = w.Size()
|
||||||
|
ChildSize = w.child.Size()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw the widget's border and everything.
|
||||||
|
w.DrawBox(e, P)
|
||||||
|
|
||||||
|
// Offset further if we are currently sunken.
|
||||||
|
var clickOffset int
|
||||||
|
if w.clicked {
|
||||||
|
clickOffset++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where to place the child widget.
|
||||||
|
moveTo := render.Point{
|
||||||
|
X: P.X + w.BoxThickness(1) + clickOffset,
|
||||||
|
Y: P.Y + w.BoxThickness(1) + clickOffset,
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're bigger than we need to be, center the child widget.
|
||||||
|
if S.Bigger(ChildSize) {
|
||||||
|
moveTo.X = P.X + (S.W / 2) - (ChildSize.W / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the text label inside.
|
||||||
|
w.child.Present(e, moveTo)
|
||||||
|
|
||||||
|
w.BaseWidget.Present(e, P)
|
||||||
|
}
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,228 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/ui/theme"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SelectBox is a kind of MenuButton which allows choosing a value from a list.
|
||||||
|
type SelectBox struct {
|
||||||
|
MenuButton
|
||||||
|
|
||||||
|
name string
|
||||||
|
|
||||||
|
// Configurables after SelectBox creation.
|
||||||
|
AlwaysChange bool // always call the Change event, even if selection not changed.
|
||||||
|
|
||||||
|
// Child widgets specific to the SelectBox.
|
||||||
|
frame *Frame
|
||||||
|
label *Label
|
||||||
|
arrow *Image
|
||||||
|
showImage *Image // User override show image
|
||||||
|
|
||||||
|
// Data storage.
|
||||||
|
textVariable string
|
||||||
|
values []SelectValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectValue holds a mapping between a text label for a SelectBox and
|
||||||
|
// its underlying value (an arbitrary data type).
|
||||||
|
type SelectValue struct {
|
||||||
|
Label string
|
||||||
|
Value interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSelectBox creates a new SelectBox.
|
||||||
|
//
|
||||||
|
// The Label configuration passed in should be used to set font styles
|
||||||
|
// and padding; the Text, TextVariable and IntVariable of the Label will
|
||||||
|
// all be ignored, as SelectBox will handle the values internally.
|
||||||
|
func NewSelectBox(name string, withLabel Label) *SelectBox {
|
||||||
|
w := &SelectBox{
|
||||||
|
name: name,
|
||||||
|
textVariable: "Choose one",
|
||||||
|
values: []SelectValue{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the label has no text of its own.
|
||||||
|
withLabel.Text = ""
|
||||||
|
withLabel.TextVariable = &w.textVariable
|
||||||
|
withLabel.IntVariable = nil
|
||||||
|
|
||||||
|
w.frame = NewFrame(name + " Frame")
|
||||||
|
w.Button.child = w.frame
|
||||||
|
|
||||||
|
w.label = NewLabel(withLabel)
|
||||||
|
w.frame.Pack(w.label, Pack{
|
||||||
|
Side: W,
|
||||||
|
})
|
||||||
|
|
||||||
|
// arrow, _ := GetGlyph(GlyphDownArrow9x9)
|
||||||
|
// w.image = ImageFromImage(arrow, )
|
||||||
|
|
||||||
|
// Configure the button's appearance.
|
||||||
|
w.Button.Configure(Config{
|
||||||
|
BorderSize: 2,
|
||||||
|
BorderStyle: BorderSunken,
|
||||||
|
Background: render.White,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set sensible default padding on the label.
|
||||||
|
if w.label.Font.Padding == 0 && w.label.Font.PadX == 0 && w.label.Font.PadY == 0 {
|
||||||
|
w.label.Font.PadX = 4
|
||||||
|
w.label.Font.PadY = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
w.IDFunc(func() string {
|
||||||
|
return fmt.Sprintf("SelectBox<%s>", name)
|
||||||
|
})
|
||||||
|
|
||||||
|
w.setup()
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetImage sets the selectbox button to show the image instead of its
|
||||||
|
// normal text label. If the image corresponds with an option in the selectbox,
|
||||||
|
// it is up to the caller to call SetImage on change and set the right image here.
|
||||||
|
//
|
||||||
|
// Provide a nil value to remove the image and show the text labels instead.
|
||||||
|
func (w *SelectBox) SetImage(img *Image) {
|
||||||
|
// Get rid of the current image.
|
||||||
|
if w.showImage != nil {
|
||||||
|
w.showImage.Hide()
|
||||||
|
w.frame.Unpack(w.showImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Are we getting a new one?
|
||||||
|
if img != nil {
|
||||||
|
w.label.Hide()
|
||||||
|
w.frame.Pack(img, Pack{
|
||||||
|
Side: W,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
w.label.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
w.showImage = img
|
||||||
|
if w.showImage != nil {
|
||||||
|
w.showImage.Show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddItem adds a new option to the SelectBox's menu.
|
||||||
|
// 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 *SelectBox) AddItem(label string, value interface{}, f func()) {
|
||||||
|
// Add this label and its value mapping to the SelectBox.
|
||||||
|
w.values = append(w.values, SelectValue{
|
||||||
|
Label: label,
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Call the inherited MenuButton.AddItem.
|
||||||
|
w.MenuButton.AddItem(label, func() {
|
||||||
|
// Set the bound label.
|
||||||
|
var changed = w.textVariable != label
|
||||||
|
w.textVariable = label
|
||||||
|
|
||||||
|
if changed || w.AlwaysChange {
|
||||||
|
w.Event(Change, EventData{
|
||||||
|
Supervisor: w.MenuButton.supervisor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// If the current text label isn't in the options, pick
|
||||||
|
// the first option.
|
||||||
|
if _, ok := w.GetValue(); !ok {
|
||||||
|
w.textVariable = w.values[0].Label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: RemoveItem()
|
||||||
|
|
||||||
|
// GetValue returns the currently selected item in the SelectBox.
|
||||||
|
//
|
||||||
|
// 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 *SelectBox) GetValue() (SelectValue, bool) {
|
||||||
|
for _, row := range w.values {
|
||||||
|
if w.textVariable == row.Label {
|
||||||
|
return row, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectValue{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValueByLabel sets the currently selected option to the given label.
|
||||||
|
func (w *SelectBox) SetValueByLabel(label string) bool {
|
||||||
|
for _, option := range w.values {
|
||||||
|
if option.Label == label {
|
||||||
|
w.textVariable = option.Label
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValue sets the currently selected option to the given value.
|
||||||
|
func (w *SelectBox) SetValue(value interface{}) bool {
|
||||||
|
for _, option := range w.values {
|
||||||
|
if option.Value == value {
|
||||||
|
w.textVariable = option.Label
|
||||||
|
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 *SelectBox) Compute(e render.Engine) {
|
||||||
|
w.MenuButton.Compute(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup the UI components and event handlers.
|
||||||
|
func (w *SelectBox) setup() {
|
||||||
|
w.Configure(Config{
|
||||||
|
BorderSize: 1,
|
||||||
|
BorderStyle: BorderSunken,
|
||||||
|
Background: theme.InputBackgroundColor,
|
||||||
|
})
|
||||||
|
|
||||||
|
w.Handle(MouseOver, func(ed EventData) error {
|
||||||
|
w.hovering = true
|
||||||
|
w.SetBackground(theme.ButtonHoverColor)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
w.Handle(MouseOut, func(ed EventData) error {
|
||||||
|
w.hovering = false
|
||||||
|
w.SetBackground(theme.InputBackgroundColor)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
w.Handle(MouseDown, func(ed EventData) error {
|
||||||
|
w.clicked = true
|
||||||
|
w.SetBackground(theme.ButtonBackgroundColor)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
w.Handle(MouseUp, func(ed EventData) error {
|
||||||
|
w.clicked = false
|
||||||
|
w.SetBackground(theme.InputBackgroundColor)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
w.Handle(Click, func(ed EventData) error {
|
||||||
|
// Are we properly configured?
|
||||||
|
if w.supervisor != nil && w.menu != nil {
|
||||||
|
w.menu.Show()
|
||||||
|
w.supervisor.PushModal(w.menu)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
// Package style provides style definitions for UI components.
|
||||||
|
package style
|
||||||
|
|
||||||
|
import "git.kirsle.net/go/render"
|
||||||
|
|
||||||
|
// Default styles for widgets without a theme.
|
||||||
|
var (
|
||||||
|
DefaultWindow = Window{
|
||||||
|
ActiveTitleBackground: render.Blue,
|
||||||
|
ActiveTitleForeground: render.White,
|
||||||
|
InactiveTitleBackground: render.DarkGrey,
|
||||||
|
InactiveTitleForeground: render.Grey,
|
||||||
|
ActiveBackground: render.Grey,
|
||||||
|
InactiveBackground: render.Grey,
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultLabel = Label{
|
||||||
|
Background: render.Invisible,
|
||||||
|
Foreground: render.Black,
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultButton = Button{
|
||||||
|
Background: render.RGBA(200, 200, 200, 255),
|
||||||
|
Foreground: render.Black,
|
||||||
|
OutlineColor: render.Black,
|
||||||
|
OutlineSize: 1,
|
||||||
|
HoverBackground: render.RGBA(200, 255, 255, 255),
|
||||||
|
HoverForeground: render.Black,
|
||||||
|
BorderStyle: BorderRaised,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Window style configuration.
|
||||||
|
type Window struct {
|
||||||
|
ActiveTitleBackground render.Color
|
||||||
|
ActiveTitleForeground render.Color
|
||||||
|
ActiveBackground render.Color
|
||||||
|
InactiveTitleBackground render.Color
|
||||||
|
InactiveTitleForeground render.Color
|
||||||
|
InactiveBackground render.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label style configuration.
|
||||||
|
type Label struct {
|
||||||
|
Background render.Color
|
||||||
|
Foreground render.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button style configuration.
|
||||||
|
type Button struct {
|
||||||
|
Background render.Color
|
||||||
|
Foreground render.Color // Labels only
|
||||||
|
OutlineColor render.Color
|
||||||
|
OutlineSize int
|
||||||
|
HoverBackground render.Color
|
||||||
|
HoverForeground render.Color
|
||||||
|
BorderStyle BorderStyle
|
||||||
|
BorderSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tooltip style configuration.
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Package style provides style definitions for UI components.
|
||||||
|
package style
|
||||||
|
|
||||||
|
// BorderStyle options for widget.SetBorderStyle()
|
||||||
|
type BorderStyle string
|
||||||
|
|
||||||
|
// Styles for a widget border.
|
||||||
|
const (
|
||||||
|
BorderNone BorderStyle = ""
|
||||||
|
BorderSolid BorderStyle = "solid"
|
||||||
|
BorderRaised = "raised"
|
||||||
|
BorderSunken = "sunken"
|
||||||
|
)
|
150
supervisor.go
|
@ -15,6 +15,7 @@ type Event int
|
||||||
const (
|
const (
|
||||||
NullEvent Event = iota
|
NullEvent Event = iota
|
||||||
MouseOver
|
MouseOver
|
||||||
|
MouseMove
|
||||||
MouseOut
|
MouseOut
|
||||||
MouseDown
|
MouseDown
|
||||||
MouseUp
|
MouseUp
|
||||||
|
@ -22,6 +23,7 @@ const (
|
||||||
KeyDown
|
KeyDown
|
||||||
KeyUp
|
KeyUp
|
||||||
KeyPress
|
KeyPress
|
||||||
|
Scroll
|
||||||
|
|
||||||
// Drag/drop event handlers.
|
// Drag/drop event handlers.
|
||||||
DragStop // if a widget is being dragged and the drag is done
|
DragStop // if a widget is being dragged and the drag is done
|
||||||
|
@ -37,6 +39,9 @@ const (
|
||||||
// Lifecycle event handlers.
|
// Lifecycle event handlers.
|
||||||
Compute // fired whenever the widget runs Compute
|
Compute // fired whenever the widget runs Compute
|
||||||
Present // fired whenever the widget runs Present
|
Present // fired whenever the widget runs Present
|
||||||
|
|
||||||
|
// Form field events.
|
||||||
|
Change
|
||||||
)
|
)
|
||||||
|
|
||||||
// EventData carries common data to event handlers.
|
// EventData carries common data to event handlers.
|
||||||
|
@ -49,6 +54,39 @@ type EventData struct {
|
||||||
|
|
||||||
// Supervisor is the reference to the supervisor who sent the event.
|
// Supervisor is the reference to the supervisor who sent the event.
|
||||||
Supervisor *Supervisor
|
Supervisor *Supervisor
|
||||||
|
|
||||||
|
// Widget is a reference to the widget receiving the event.
|
||||||
|
Widget Widget
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func (ed EventData) RelativePoint() render.Point {
|
||||||
|
if ed.Widget == nil {
|
||||||
|
return render.NewPoint(-1, -1)
|
||||||
|
}
|
||||||
|
abs := AbsolutePosition(ed.Widget)
|
||||||
|
return render.NewPoint(ed.Point.X-abs.X, ed.Point.Y-abs.Y)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Supervisor keeps track of widgets of interest to notify them about
|
// Supervisor keeps track of widgets of interest to notify them about
|
||||||
|
@ -67,8 +105,10 @@ type Supervisor struct {
|
||||||
|
|
||||||
// List of window focus history for Window Manager.
|
// List of window focus history for Window Manager.
|
||||||
winFocus *FocusedWindow
|
winFocus *FocusedWindow
|
||||||
winTop *FocusedWindow // pointer to top-most window
|
|
||||||
winBottom *FocusedWindow // pointer to bottom-most window
|
winBottom *FocusedWindow // pointer to bottom-most window
|
||||||
|
|
||||||
|
// Widgets that we should draw on top, such as Tooltips.
|
||||||
|
onTop []Widget
|
||||||
}
|
}
|
||||||
|
|
||||||
// WidgetSlot holds a widget with a unique ID number in a sorted list.
|
// WidgetSlot holds a widget with a unique ID number in a sorted list.
|
||||||
|
@ -84,6 +124,7 @@ func NewSupervisor() *Supervisor {
|
||||||
hovering: map[int]interface{}{},
|
hovering: map[int]interface{}{},
|
||||||
clicked: map[int]bool{},
|
clicked: map[int]bool{},
|
||||||
modals: []Widget{},
|
modals: []Widget{},
|
||||||
|
onTop: []Widget{},
|
||||||
dd: NewDragDrop(),
|
dd: NewDragDrop(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,7 +188,8 @@ func (s *Supervisor) Loop(ev *event.State) error {
|
||||||
// The mouse has been released. TODO: make mouse button important?
|
// The mouse has been released. TODO: make mouse button important?
|
||||||
for _, child := range hovering {
|
for _, child := range hovering {
|
||||||
child.widget.Event(Drop, EventData{
|
child.widget.Event(Drop, EventData{
|
||||||
Point: XY,
|
Widget: child.widget,
|
||||||
|
Point: XY,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
s.DragStop()
|
s.DragStop()
|
||||||
|
@ -155,7 +197,8 @@ func (s *Supervisor) Loop(ev *event.State) error {
|
||||||
// If we have a target widget being dragged, send it mouse events.
|
// If we have a target widget being dragged, send it mouse events.
|
||||||
if target := s.dd.Widget(); target != nil {
|
if target := s.dd.Widget(); target != nil {
|
||||||
target.Event(DragMove, EventData{
|
target.Event(DragMove, EventData{
|
||||||
Point: XY,
|
Widget: target,
|
||||||
|
Point: XY,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -183,7 +226,7 @@ func (s *Supervisor) Loop(ev *event.State) error {
|
||||||
if err == ErrStopPropagation || handled {
|
if err == ErrStopPropagation || handled {
|
||||||
// A widget in the active window has accepted an event. Do not pass
|
// A widget in the active window has accepted an event. Do not pass
|
||||||
// the event also to lower widgets.
|
// the event also to lower widgets.
|
||||||
return err
|
return ErrStopPropagation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,20 +274,22 @@ func (s *Supervisor) Hovering(cursor render.Point) (hovering, outside []WidgetSl
|
||||||
// cursor, transmit mouse events to the widgets.
|
// cursor, transmit mouse events to the widgets.
|
||||||
//
|
//
|
||||||
// This function has two use cases:
|
// This function has two use cases:
|
||||||
// - In runWindowEvents where we run events for the top-most focused window of
|
// - In runWindowEvents where we run events for the top-most focused window of
|
||||||
// the window manager.
|
// the window manager.
|
||||||
// - In Supervisor.Loop() for the widgets that are NOT owned by a managed
|
// - In Supervisor.Loop() for the widgets that are NOT owned by a managed
|
||||||
// window, so that these widgets always get events.
|
// window, so that these widgets always get events.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// XY (Point): mouse cursor position as calculated in Loop()
|
//
|
||||||
// ev, hovering, outside: values from Loop(), self explanatory.
|
// XY (Point): mouse cursor position as calculated in Loop()
|
||||||
// behavior: indicates how this method is being used.
|
// ev, hovering, outside: values from Loop(), self explanatory.
|
||||||
|
// behavior: indicates how this method is being used.
|
||||||
//
|
//
|
||||||
// behavior options:
|
// behavior options:
|
||||||
// 0: widgets NOT part of a managed window. On this pass, if a widget IS
|
//
|
||||||
// a part of a window, it gets no events triggered.
|
// 0: widgets NOT part of a managed window. On this pass, if a widget IS
|
||||||
// 1: widgets are part of the active focused window.
|
// 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,
|
func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State,
|
||||||
hovering, outside []WidgetSlot, toFocusedWindow bool) (bool, error) {
|
hovering, outside []WidgetSlot, toFocusedWindow bool) (bool, error) {
|
||||||
// Do we run any events?
|
// Do we run any events?
|
||||||
|
@ -265,7 +310,7 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State,
|
||||||
// the bounding box of the active focused window. Prevents clicking "thru"
|
// the bounding box of the active focused window. Prevents clicking "thru"
|
||||||
// the window and activating widgets/other windows behind it.
|
// the window and activating widgets/other windows behind it.
|
||||||
var cursorInsideFocusedWindow bool
|
var cursorInsideFocusedWindow bool
|
||||||
if !toFocusedWindow && s.winFocus != nil {
|
if !toFocusedWindow && s.winFocus != nil && !s.winFocus.window.Hidden() {
|
||||||
// Get the bounding box of the focused window.
|
// Get the bounding box of the focused window.
|
||||||
if XY.Inside(AbsoluteRect(s.winFocus.window)) {
|
if XY.Inside(AbsoluteRect(s.winFocus.window)) {
|
||||||
cursorInsideFocusedWindow = true
|
cursorInsideFocusedWindow = true
|
||||||
|
@ -343,29 +388,42 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State,
|
||||||
// Cursor has intersected the widget.
|
// Cursor has intersected the widget.
|
||||||
if _, ok := s.hovering[id]; !ok {
|
if _, ok := s.hovering[id]; !ok {
|
||||||
handle(w.Event(MouseOver, EventData{
|
handle(w.Event(MouseOver, EventData{
|
||||||
Point: XY,
|
Widget: w,
|
||||||
|
Point: XY,
|
||||||
}))
|
}))
|
||||||
s.hovering[id] = nil
|
s.hovering[id] = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
isClicked, _ := s.clicked[id]
|
isClicked := s.clicked[id]
|
||||||
if ev.Button1 {
|
if ev.Button1 {
|
||||||
if !isClicked {
|
if !isClicked {
|
||||||
err := w.Event(MouseDown, EventData{
|
err := w.Event(MouseDown, EventData{
|
||||||
Point: XY,
|
Widget: w,
|
||||||
|
Point: XY,
|
||||||
})
|
})
|
||||||
handle(err)
|
handle(err)
|
||||||
s.clicked[id] = true
|
s.clicked[id] = true
|
||||||
}
|
}
|
||||||
} else if isClicked {
|
} else if isClicked {
|
||||||
handle(w.Event(MouseUp, EventData{
|
handle(w.Event(MouseUp, EventData{
|
||||||
Point: XY,
|
Widget: w,
|
||||||
|
Point: XY,
|
||||||
}))
|
}))
|
||||||
handle(w.Event(Click, EventData{
|
handle(w.Event(Click, EventData{
|
||||||
Point: XY,
|
Widget: w,
|
||||||
|
Point: XY,
|
||||||
}))
|
}))
|
||||||
delete(s.clicked, id)
|
delete(s.clicked, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mouse movement. NOTE: it is intentional that this fires on
|
||||||
|
// every tick even if XY was the same as last time.
|
||||||
|
handle(w.Event(MouseMove, EventData{
|
||||||
|
Widget: w,
|
||||||
|
Point: XY,
|
||||||
|
Clicked: ev.Button1,
|
||||||
|
}))
|
||||||
|
|
||||||
}
|
}
|
||||||
for _, child := range outside {
|
for _, child := range outside {
|
||||||
var (
|
var (
|
||||||
|
@ -384,14 +442,16 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State,
|
||||||
// Cursor is not intersecting the widget.
|
// Cursor is not intersecting the widget.
|
||||||
if _, ok := s.hovering[id]; ok {
|
if _, ok := s.hovering[id]; ok {
|
||||||
handle(w.Event(MouseOut, EventData{
|
handle(w.Event(MouseOut, EventData{
|
||||||
Point: XY,
|
Widget: w,
|
||||||
|
Point: XY,
|
||||||
}))
|
}))
|
||||||
delete(s.hovering, id)
|
delete(s.hovering, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := s.clicked[id]; ok {
|
if _, ok := s.clicked[id]; ok {
|
||||||
handle(w.Event(MouseUp, EventData{
|
handle(w.Event(MouseUp, EventData{
|
||||||
Point: XY,
|
Widget: w,
|
||||||
|
Point: XY,
|
||||||
}))
|
}))
|
||||||
delete(s.clicked, id)
|
delete(s.clicked, id)
|
||||||
}
|
}
|
||||||
|
@ -407,6 +467,12 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there was a modal, return stopPropagation (so callers that manage
|
||||||
|
// events externally of go/ui can see that a modal intercepted events)
|
||||||
|
if modal != nil {
|
||||||
|
return ranEvents, ErrStopPropagation
|
||||||
|
}
|
||||||
|
|
||||||
// If a stopPropagation was called, return it up the stack.
|
// If a stopPropagation was called, return it up the stack.
|
||||||
if stopPropagation {
|
if stopPropagation {
|
||||||
return ranEvents, ErrStopPropagation
|
return ranEvents, ErrStopPropagation
|
||||||
|
@ -451,6 +517,16 @@ func (s *Supervisor) Present(e render.Engine) {
|
||||||
modal.Present(e, modal.Point())
|
modal.Present(e, modal.Point())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render any "on top" widgets like Tooltips.
|
||||||
|
if len(s.onTop) > 0 {
|
||||||
|
for _, widget := range s.onTop {
|
||||||
|
if widget.Hidden() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
widget.Present(e, widget.Point())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a widget to be supervised. Has no effect if the widget is already
|
// Add a widget to be supervised. Has no effect if the widget is already
|
||||||
|
@ -510,3 +586,33 @@ func (s *Supervisor) PopModal(w Widget) bool {
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetModal returns the modal on the top of the stack, or nil if there is
|
||||||
|
// no modal on top.
|
||||||
|
func (s *Supervisor) GetModal() Widget {
|
||||||
|
if len(s.modals) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.modals[len(s.modals)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
DrawOnTop gives the Supervisor a widget to manage the presentation of, for
|
||||||
|
example the Tooltip.
|
||||||
|
|
||||||
|
If you call Supervisor.Present() in your program's main loop, it will draw the
|
||||||
|
widgets that it manages, such as Windows, Menus and Tooltips. Call that function
|
||||||
|
last in your main loop, and these things are drawn on top of the rest of your
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
func (s *Supervisor) DrawOnTop(w Widget) {
|
||||||
|
s.onTop = append(s.onTop, w)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,300 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/ui/style"
|
||||||
|
"git.kirsle.net/go/ui/theme"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TabFrame is a tabbed notebook of multiple frames showing
|
||||||
|
// tab names along the top and clicking them reveals each
|
||||||
|
// named tab.
|
||||||
|
type TabFrame struct {
|
||||||
|
Name string
|
||||||
|
Frame
|
||||||
|
|
||||||
|
supervisor *Supervisor
|
||||||
|
style *style.Button
|
||||||
|
|
||||||
|
// Child widgets.
|
||||||
|
header *Frame
|
||||||
|
content *Frame
|
||||||
|
tabButtons []*Button
|
||||||
|
tabFrames []*Frame
|
||||||
|
currentTabKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTabFrame creates a new Frame.
|
||||||
|
func NewTabFrame(name string) *TabFrame {
|
||||||
|
w := &TabFrame{
|
||||||
|
Name: name,
|
||||||
|
style: Theme.TabFrame,
|
||||||
|
header: NewFrame(name + " Header"),
|
||||||
|
content: NewFrame(name + " Content"),
|
||||||
|
tabButtons: []*Button{},
|
||||||
|
tabFrames: []*Frame{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the root frame of this widget.
|
||||||
|
w.Frame.Setup()
|
||||||
|
|
||||||
|
// Pack the high-level layout into the root frame.
|
||||||
|
// Only root needs to Present for this widget.
|
||||||
|
w.Frame.Pack(w.header, Pack{
|
||||||
|
Side: N,
|
||||||
|
FillX: true,
|
||||||
|
})
|
||||||
|
w.Frame.Pack(w.content, Pack{
|
||||||
|
Side: N,
|
||||||
|
Fill: true,
|
||||||
|
Expand: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
w.SetBackground(render.RGBA(1, 0, 0, 0)) // invisible default BG
|
||||||
|
w.IDFunc(func() string {
|
||||||
|
return fmt.Sprintf("TabFrame<%s>",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTab creates a new content tab. The key is a unique identifier
|
||||||
|
// for the tab and is how the TabFrame knows which tab is selected.
|
||||||
|
//
|
||||||
|
// The child widget would probably be a Label or Image but could be
|
||||||
|
// any other kind of widget.
|
||||||
|
//
|
||||||
|
// The first tab added becomes the selected tab by default.
|
||||||
|
func (w *TabFrame) AddTab(key string, child Widget) *Frame {
|
||||||
|
// Create the tab button for this tab.
|
||||||
|
button := NewButton(key, child)
|
||||||
|
button.SetStyle(w.style)
|
||||||
|
button.FixedColor = true
|
||||||
|
button.SetOutlineSize(0)
|
||||||
|
button.SetBorderSize(1)
|
||||||
|
w.setButtonStyle(button, len(w.tabButtons) == 0)
|
||||||
|
w.header.Pack(button, Pack{
|
||||||
|
Side: W,
|
||||||
|
})
|
||||||
|
|
||||||
|
button.Handle(MouseDown, func(ed EventData) error {
|
||||||
|
w.SetTab(key)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create the frame of the tab's body.
|
||||||
|
frame := NewFrame(key)
|
||||||
|
frame.Configure(Config{
|
||||||
|
BorderSize: w.style.BorderSize,
|
||||||
|
Background: w.style.Background,
|
||||||
|
BorderStyle: BorderRaised,
|
||||||
|
})
|
||||||
|
if len(w.tabFrames) > 0 {
|
||||||
|
frame.Hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack this frame into the content part of the widget.
|
||||||
|
w.content.Pack(frame, Pack{
|
||||||
|
Side: N,
|
||||||
|
FillX: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
w.tabButtons = append(w.tabButtons, button)
|
||||||
|
w.tabFrames = append(w.tabFrames, frame)
|
||||||
|
return frame
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTabsHidden can hide the tab buttons and reveal only their frames.
|
||||||
|
// It would be up to the caller to SetTab between the frames, using the
|
||||||
|
// TabFrame only for placement and tab handling.
|
||||||
|
func (w *TabFrame) SetTabsHidden(hidden bool) {
|
||||||
|
if hidden {
|
||||||
|
w.header.Hide()
|
||||||
|
} else {
|
||||||
|
w.header.Show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header returns access to the ui.Frame that holds the tab buttons. Use
|
||||||
|
// at your own risk -- the UI arrangement in this Frame is not guaranteed
|
||||||
|
// stable.
|
||||||
|
func (w *TabFrame) Header() *Frame {
|
||||||
|
return w.header
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the tab style between active and inactive
|
||||||
|
func (w *TabFrame) setButtonStyle(button *Button, active bool) {
|
||||||
|
var style = button.GetStyle()
|
||||||
|
if active {
|
||||||
|
button.SetBackground(style.Background)
|
||||||
|
button.SetBorderStyle(BorderRaised)
|
||||||
|
if label, ok := button.child.(*Label); ok {
|
||||||
|
label.Font.Color = style.Foreground
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
button.SetBackground(style.Background.Darken(theme.BorderColorOffset))
|
||||||
|
button.SetBorderStyle(BorderSolid)
|
||||||
|
if label, ok := button.child.(*Label); ok {
|
||||||
|
label.Font.Color = style.Foreground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTab changes the selected tab to the new value. If the
|
||||||
|
// tab doesn't exist, the first tab is selected.
|
||||||
|
func (w *TabFrame) SetTab(key string) bool {
|
||||||
|
var found bool
|
||||||
|
for i, frame := range w.tabFrames {
|
||||||
|
button := w.tabButtons[i]
|
||||||
|
if frame.Name == key {
|
||||||
|
frame.Show()
|
||||||
|
w.setButtonStyle(button, true)
|
||||||
|
w.currentTabKey = key
|
||||||
|
found = true
|
||||||
|
} else {
|
||||||
|
frame.Hide()
|
||||||
|
w.setButtonStyle(button, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found && len(w.tabFrames) > 0 {
|
||||||
|
w.tabFrames[0].Show()
|
||||||
|
w.currentTabKey = w.tabFrames[0].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supervise activates the tab frame using your supervisor. If you
|
||||||
|
// don't call this, the tab buttons won't be clickable!
|
||||||
|
//
|
||||||
|
// Call this AFTER adding all tabs. This function calls Supervisor.Add
|
||||||
|
// on all tab buttons.
|
||||||
|
func (w *TabFrame) Supervise(supervisor *Supervisor) {
|
||||||
|
for _, button := range w.tabButtons {
|
||||||
|
supervisor.Add(button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStyle controls the visual styling of the tab button bar.
|
||||||
|
func (w *TabFrame) SetStyle(style *style.Button) {
|
||||||
|
w.style = style
|
||||||
|
for _, button := range w.tabButtons {
|
||||||
|
button.SetStyle(style)
|
||||||
|
w.setButtonStyle(button, !button.Hidden())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the size of the Frame.
|
||||||
|
func (w *TabFrame) Compute(e render.Engine) {
|
||||||
|
// Compute all the child frames.
|
||||||
|
w.Frame.Compute(e)
|
||||||
|
|
||||||
|
// Call the BaseWidget Compute in case we have subscribers.
|
||||||
|
w.BaseWidget.Compute(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present the Frame.
|
||||||
|
func (w *TabFrame) Present(e render.Engine, P render.Point) {
|
||||||
|
if w.Hidden() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
S = w.Size()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw the widget's border and everything.
|
||||||
|
w.DrawBox(e, P)
|
||||||
|
|
||||||
|
// Draw the background color.
|
||||||
|
e.DrawBox(w.Background(), render.Rect{
|
||||||
|
X: P.X + w.BoxThickness(1),
|
||||||
|
Y: P.Y + w.BoxThickness(1),
|
||||||
|
W: S.W - w.BoxThickness(2),
|
||||||
|
H: S.H - w.BoxThickness(2),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Present the root frame.
|
||||||
|
w.Frame.Present(e, P)
|
||||||
|
|
||||||
|
// Draw the borders over the tabs.
|
||||||
|
w.presentBorders(e, P)
|
||||||
|
|
||||||
|
// Call the BaseWidget Present in case we have subscribers.
|
||||||
|
w.BaseWidget.Present(e, P)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
presentBorders handles drawing the borders around tab buttons.
|
||||||
|
|
||||||
|
The tabs are simple Button widgets but drawn with no borders. Instead,
|
||||||
|
borders are painted on post-hoc in the Present function.
|
||||||
|
*/
|
||||||
|
func (w *TabFrame) presentBorders(e render.Engine, P render.Point) {
|
||||||
|
if len(w.tabButtons) == 0 || w.header.Hidden() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prep some variables.
|
||||||
|
var (
|
||||||
|
// The 1st and last tab button widgets.
|
||||||
|
first = w.tabButtons[0]
|
||||||
|
last = w.tabButtons[len(w.tabButtons)-1]
|
||||||
|
topLeft = AbsolutePosition(first)
|
||||||
|
bottomRight = AbsolutePosition(last)
|
||||||
|
|
||||||
|
// The absolute bounding box of the tabs part of the UI,
|
||||||
|
// from the top-left corner of Tab #1 to the bottom-right
|
||||||
|
// corner of the final tab.
|
||||||
|
bounding = render.Rect{
|
||||||
|
X: P.X, //topLeft.X + first.BoxThickness(4),
|
||||||
|
Y: P.Y, //topLeft.Y + first.BoxThickness(4),
|
||||||
|
W: bottomRight.X + last.Size().W - topLeft.X,
|
||||||
|
H: bottomRight.Y + last.Size().H - topLeft.Y,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The very bottom edge of the whole tab bar,
|
||||||
|
// to overlap the BorderSize=1 along their buttons.
|
||||||
|
bottomLine = []render.Point{
|
||||||
|
render.NewPoint(P.X+1, bounding.Y+bounding.H-1),
|
||||||
|
render.NewPoint(bounding.X+bounding.W-1, bounding.Y+bounding.H-1),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw a shadow border on all the inactive tabs' right edges,
|
||||||
|
// so they don't all blend together in solid grey.
|
||||||
|
// Note: the active button has a BorderSize=1 and others are 0.
|
||||||
|
for i, button := range w.tabButtons {
|
||||||
|
if button.Name != w.currentTabKey {
|
||||||
|
// If it immediately precedes the current tab, do not draw the line,
|
||||||
|
// it would cover the highlight color of the current tab's button.
|
||||||
|
if i+1 < len(w.tabButtons) && w.tabButtons[i+1].Name == w.currentTabKey {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
abs = AbsolutePosition(button)
|
||||||
|
size = button.BoxSize()
|
||||||
|
points = []render.Point{
|
||||||
|
render.NewPoint(abs.X+size.W-1, abs.Y+2),
|
||||||
|
render.NewPoint(abs.X+size.W-1, abs.Y+size.H-2),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
e.DrawLine(button.Background().Darken(theme.BorderColorOffset), points[0], points[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erase the button edge from all tabs.
|
||||||
|
e.DrawLine(w.style.Background, bottomLine[0], bottomLine[1])
|
||||||
|
e.DrawBox(w.style.Background, render.Rect{
|
||||||
|
X: bottomLine[0].X + 1,
|
||||||
|
Y: bottomLine[0].Y,
|
||||||
|
W: bounding.W - 2,
|
||||||
|
H: 4,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import "git.kirsle.net/go/ui/theme"
|
||||||
|
|
||||||
|
// Theme sets the default theme used when creating new widgets.
|
||||||
|
var Theme = theme.Default
|
|
@ -1,12 +1,100 @@
|
||||||
package theme
|
package theme
|
||||||
|
|
||||||
import "git.kirsle.net/go/render"
|
import (
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/ui/style"
|
||||||
|
)
|
||||||
|
|
||||||
// Color schemes.
|
// Color schemes.
|
||||||
var (
|
var (
|
||||||
ButtonBackgroundColor = render.RGBA(200, 200, 200, 255)
|
ButtonBackgroundColor = render.RGBA(200, 200, 200, 255)
|
||||||
ButtonHoverColor = render.RGBA(200, 255, 255, 255)
|
ButtonHoverColor = render.RGBA(200, 255, 255, 255)
|
||||||
ButtonOutlineColor = render.Black
|
ButtonOutlineColor = render.Black
|
||||||
|
InputBackgroundColor = render.White
|
||||||
|
|
||||||
BorderColorOffset = 40
|
BorderColorOffset = 40
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Theme is a collection of styles for various built-in widgets.
|
||||||
|
type Theme struct {
|
||||||
|
Name string
|
||||||
|
Window *style.Window
|
||||||
|
Label *style.Label
|
||||||
|
Button *style.Button
|
||||||
|
ListBox *style.ListBox
|
||||||
|
Tooltip *style.Tooltip
|
||||||
|
TabFrame *style.Button
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default theme.
|
||||||
|
var Default = Theme{
|
||||||
|
Name: "Default",
|
||||||
|
Label: &style.DefaultLabel,
|
||||||
|
Button: &style.DefaultButton,
|
||||||
|
ListBox: &style.DefaultListBox,
|
||||||
|
Tooltip: &style.DefaultTooltip,
|
||||||
|
TabFrame: &style.DefaultButton,
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultFlat is a flat version of the default theme.
|
||||||
|
var DefaultFlat = Theme{
|
||||||
|
Name: "DefaultFlat",
|
||||||
|
Button: &style.Button{
|
||||||
|
Background: style.DefaultButton.Background,
|
||||||
|
Foreground: style.DefaultButton.Foreground,
|
||||||
|
OutlineColor: style.DefaultButton.OutlineColor,
|
||||||
|
OutlineSize: 1,
|
||||||
|
HoverBackground: style.DefaultButton.HoverBackground,
|
||||||
|
HoverForeground: style.DefaultButton.HoverForeground,
|
||||||
|
BorderStyle: style.BorderSolid,
|
||||||
|
BorderSize: 2,
|
||||||
|
},
|
||||||
|
TabFrame: &style.Button{
|
||||||
|
Background: style.DefaultButton.Background,
|
||||||
|
Foreground: style.DefaultButton.Foreground,
|
||||||
|
OutlineColor: style.DefaultButton.OutlineColor,
|
||||||
|
OutlineSize: 1,
|
||||||
|
HoverBackground: style.DefaultButton.HoverBackground,
|
||||||
|
HoverForeground: style.DefaultButton.HoverForeground,
|
||||||
|
BorderStyle: style.BorderSolid,
|
||||||
|
BorderSize: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultDark is a dark version of the default theme.
|
||||||
|
var DefaultDark = Theme{
|
||||||
|
Name: "DefaultDark",
|
||||||
|
Label: &style.Label{
|
||||||
|
Foreground: render.Grey,
|
||||||
|
},
|
||||||
|
Window: &style.Window{
|
||||||
|
ActiveTitleBackground: render.Red,
|
||||||
|
ActiveTitleForeground: render.White,
|
||||||
|
InactiveTitleBackground: render.DarkGrey,
|
||||||
|
InactiveTitleForeground: render.Grey,
|
||||||
|
ActiveBackground: render.Black,
|
||||||
|
InactiveBackground: render.Black,
|
||||||
|
},
|
||||||
|
Button: &style.Button{
|
||||||
|
Background: render.Black,
|
||||||
|
Foreground: render.Grey,
|
||||||
|
OutlineColor: render.DarkGrey,
|
||||||
|
OutlineSize: 1,
|
||||||
|
HoverBackground: render.Grey,
|
||||||
|
BorderStyle: style.BorderRaised,
|
||||||
|
BorderSize: 2,
|
||||||
|
},
|
||||||
|
Tooltip: &style.Tooltip{
|
||||||
|
Background: render.RGBA(60, 60, 60, 230),
|
||||||
|
Foreground: render.Cyan,
|
||||||
|
},
|
||||||
|
TabFrame: &style.Button{
|
||||||
|
Background: render.DarkGrey,
|
||||||
|
Foreground: render.Grey,
|
||||||
|
OutlineColor: render.DarkGrey,
|
||||||
|
OutlineSize: 1,
|
||||||
|
HoverBackground: render.Grey,
|
||||||
|
BorderStyle: style.BorderRaised,
|
||||||
|
BorderSize: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
44
tooltip.go
|
@ -5,13 +5,18 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/ui/style"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
precomputeArrows()
|
precomputeArrows()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tooltip attaches a mouse-over popup to another widget.
|
/*
|
||||||
|
Tooltip attaches a mouse-over popup to another widget.
|
||||||
|
|
||||||
|
|
||||||
|
*/
|
||||||
type Tooltip struct {
|
type Tooltip struct {
|
||||||
BaseWidget
|
BaseWidget
|
||||||
|
|
||||||
|
@ -19,7 +24,9 @@ type Tooltip struct {
|
||||||
Text string // Text to show in the tooltip.
|
Text string // Text to show in the tooltip.
|
||||||
TextVariable *string // String pointer instead of text.
|
TextVariable *string // String pointer instead of text.
|
||||||
Edge Edge // side to display tooltip on
|
Edge Edge // side to display tooltip on
|
||||||
|
supervisor *Supervisor
|
||||||
|
|
||||||
|
style *style.Tooltip
|
||||||
target Widget
|
target Widget
|
||||||
lineHeight int
|
lineHeight int
|
||||||
font render.Text
|
font render.Text
|
||||||
|
@ -66,7 +73,9 @@ func NewTooltip(target Widget, tt Tooltip) *Tooltip {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
target.Handle(Present, func(ed EventData) error {
|
target.Handle(Present, func(ed EventData) error {
|
||||||
w.Present(ed.Engine, w.Point())
|
if w.supervisor == nil {
|
||||||
|
w.Present(ed.Engine, w.Point())
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -74,9 +83,40 @@ func NewTooltip(target Widget, tt Tooltip) *Tooltip {
|
||||||
return fmt.Sprintf(`Tooltip<"%s">`, w.Value())
|
return fmt.Sprintf(`Tooltip<"%s">`, w.Value())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
w.SetStyle(Theme.Tooltip)
|
||||||
|
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Supervise the tooltip widget. This will put the rendering of this widget under the
|
||||||
|
Supervisor's care to be drawn "on top" of all other widgets. Your main loop should
|
||||||
|
call the Supervisor.Present() function lastly so that things managed by it (such as
|
||||||
|
Windows, Menus and Tooltips) draw on top of everything else.
|
||||||
|
|
||||||
|
If you don't call this, the Tooltip by default will present when its attached widget
|
||||||
|
presents (if moused over and tooltip is to be visible). This alone is fine in many
|
||||||
|
simple use cases, but in a densely packed UI layout and depending on the Edge the
|
||||||
|
tooltip draws at, it may get over-drawn by other widgets and not appear "on top."
|
||||||
|
*/
|
||||||
|
func (w *Tooltip) Supervise(s *Supervisor) {
|
||||||
|
w.supervisor = s
|
||||||
|
|
||||||
|
// Supervisor will manage our presentation and draw us "on top"
|
||||||
|
w.supervisor.DrawOnTop(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStyle sets the tooltip's default style.
|
||||||
|
func (w *Tooltip) SetStyle(v *style.Tooltip) {
|
||||||
|
if v == nil {
|
||||||
|
v = &style.DefaultTooltip
|
||||||
|
}
|
||||||
|
|
||||||
|
w.style = v
|
||||||
|
w.SetBackground(w.style.Background)
|
||||||
|
w.font.Color = w.style.Foreground
|
||||||
|
}
|
||||||
|
|
||||||
// Value returns the current text displayed in the tooltop, whether from the
|
// Value returns the current text displayed in the tooltop, whether from the
|
||||||
// configured Text or the TextVariable pointer.
|
// configured Text or the TextVariable pointer.
|
||||||
func (w *Tooltip) Value() string {
|
func (w *Tooltip) Value() string {
|
||||||
|
|
|
@ -21,10 +21,24 @@ func ExampleTooltip() {
|
||||||
// Add a tooltip to it. The tooltip attaches itself to the button's
|
// Add a tooltip to it. The tooltip attaches itself to the button's
|
||||||
// MouseOver, MouseOut, Compute and Present handlers -- you don't need to
|
// MouseOver, MouseOut, Compute and Present handlers -- you don't need to
|
||||||
// place the tooltip inside the window or parent frame.
|
// place the tooltip inside the window or parent frame.
|
||||||
ui.NewTooltip(btn, ui.Tooltip{
|
tt := ui.NewTooltip(btn, ui.Tooltip{
|
||||||
Text: "This is a tooltip that pops up\non mouse hover!",
|
Text: "This is a tooltip that pops up\non mouse hover!",
|
||||||
Edge: ui.Right,
|
Edge: ui.Right,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Notice: by default (with just the above code), the Tooltip will present
|
||||||
|
// when its target widget presents. For densely packed UIs, the Tooltip may
|
||||||
|
// be drawn "below" a neighboring widget, e.g. for horizontally packed buttons
|
||||||
|
// where the Tooltip is on the Right: the tooltip for the left-most button
|
||||||
|
// would present when the button does, but then the next button over will present
|
||||||
|
// and overwrite the tooltip.
|
||||||
|
//
|
||||||
|
// For many simple UIs you can arrange your widgets and tooltip edge to
|
||||||
|
// avoid this, but to guarantee the Tooltip always draws "on top", you
|
||||||
|
// need to give it your Supervisor so it can register itself into its
|
||||||
|
// Present stage (similar to window management). Be sure to call Supervisor.Present()
|
||||||
|
// lastly in your main loop.
|
||||||
|
tt.Supervise(mw.Supervisor())
|
||||||
|
|
||||||
mw.MainLoop()
|
mw.MainLoop()
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,9 @@ type Widget interface {
|
||||||
|
|
||||||
// Render the final widget onto the drawing engine.
|
// Render the final widget onto the drawing engine.
|
||||||
Present(render.Engine, render.Point)
|
Present(render.Engine, render.Point)
|
||||||
|
|
||||||
|
// Destroy: implement this if you have resources to free up on teardown.
|
||||||
|
Destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config holds common base widget configs for quick configuration.
|
// Config holds common base widget configs for quick configuration.
|
||||||
|
@ -523,3 +526,6 @@ func (w *BaseWidget) Handle(event Event, fn func(EventData) error) {
|
||||||
|
|
||||||
// OnMouseOut should be overridden on widgets who want this event.
|
// OnMouseOut should be overridden on widgets who want this event.
|
||||||
func (w *BaseWidget) OnMouseOut(render.Point) {}
|
func (w *BaseWidget) OnMouseOut(render.Point) {}
|
||||||
|
|
||||||
|
// Destroy does nothing on the base widget. Implement it for widgets which need it.
|
||||||
|
func (w *BaseWidget) Destroy() {}
|
||||||
|
|
89
window.go
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/ui/style"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Window is a frame with a title bar.
|
// Window is a frame with a title bar.
|
||||||
|
@ -19,6 +20,7 @@ type Window struct {
|
||||||
InactiveTitleForeground render.Color
|
InactiveTitleForeground render.Color
|
||||||
|
|
||||||
// Private widgets.
|
// Private widgets.
|
||||||
|
style *style.Window
|
||||||
body *Frame
|
body *Frame
|
||||||
titleBar *Frame
|
titleBar *Frame
|
||||||
titleLabel *Label
|
titleLabel *Label
|
||||||
|
@ -58,12 +60,6 @@ func NewWindow(title string) *Window {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
w.body.Configure(Config{
|
|
||||||
Background: render.Grey,
|
|
||||||
BorderSize: 2,
|
|
||||||
BorderStyle: BorderRaised,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Title bar widget.
|
// Title bar widget.
|
||||||
titleBar, titleLabel := w.setupTitleBar()
|
titleBar, titleLabel := w.setupTitleBar()
|
||||||
w.body.Pack(titleBar, Pack{
|
w.body.Pack(titleBar, Pack{
|
||||||
|
@ -87,9 +83,32 @@ func NewWindow(title string) *Window {
|
||||||
// Set up parent/child relationships
|
// Set up parent/child relationships
|
||||||
w.body.SetParent(w)
|
w.body.SetParent(w)
|
||||||
|
|
||||||
|
w.SetStyle(Theme.Window)
|
||||||
|
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetStyle sets the window style.
|
||||||
|
func (w *Window) SetStyle(v *style.Window) {
|
||||||
|
if v == nil {
|
||||||
|
v = &style.DefaultWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
w.style = v
|
||||||
|
w.body.Configure(Config{
|
||||||
|
Background: w.style.ActiveBackground,
|
||||||
|
BorderSize: 2,
|
||||||
|
BorderStyle: BorderRaised,
|
||||||
|
})
|
||||||
|
if w.focused {
|
||||||
|
w.titleBar.SetBackground(w.style.ActiveTitleBackground)
|
||||||
|
w.titleLabel.Font.Color = w.style.ActiveTitleForeground
|
||||||
|
} else {
|
||||||
|
w.titleBar.SetBackground(w.style.InactiveTitleBackground)
|
||||||
|
w.titleLabel.Font.Color = w.style.InactiveTitleForeground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// setupTitlebar creates the title bar frame of the window.
|
// setupTitlebar creates the title bar frame of the window.
|
||||||
func (w *Window) setupTitleBar() (*Frame, *Label) {
|
func (w *Window) setupTitleBar() (*Frame, *Label) {
|
||||||
frame := NewFrame("Titlebar for Window: " + w.Title)
|
frame := NewFrame("Titlebar for Window: " + w.Title)
|
||||||
|
@ -102,7 +121,7 @@ func (w *Window) setupTitleBar() (*Frame, *Label) {
|
||||||
TextVariable: &w.Title,
|
TextVariable: &w.Title,
|
||||||
Font: render.Text{
|
Font: render.Text{
|
||||||
Color: w.ActiveTitleForeground,
|
Color: w.ActiveTitleForeground,
|
||||||
Size: 10,
|
Size: 11,
|
||||||
Stroke: w.ActiveTitleBackground.Darken(40),
|
Stroke: w.ActiveTitleBackground.Darken(40),
|
||||||
Padding: 2,
|
Padding: 2,
|
||||||
},
|
},
|
||||||
|
@ -260,12 +279,12 @@ func (w *Window) SetFocus(v bool) {
|
||||||
|
|
||||||
// Update the title bar colors.
|
// Update the title bar colors.
|
||||||
var (
|
var (
|
||||||
bg = w.ActiveTitleBackground
|
bg = w.style.ActiveTitleBackground
|
||||||
fg = w.ActiveTitleForeground
|
fg = w.style.ActiveTitleForeground
|
||||||
)
|
)
|
||||||
if !w.focused {
|
if !w.focused {
|
||||||
bg = w.InactiveTitleBackground
|
bg = w.style.InactiveTitleBackground
|
||||||
fg = w.InactiveTitleForeground
|
fg = w.style.InactiveTitleForeground
|
||||||
}
|
}
|
||||||
w.titleBar.SetBackground(bg)
|
w.titleBar.SetBackground(bg)
|
||||||
w.titleLabel.Font.Color = fg
|
w.titleLabel.Font.Color = fg
|
||||||
|
@ -296,6 +315,26 @@ func (w *Window) SetMaximized(v bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Size returns the window's size (the size of its underlying body frame,
|
||||||
|
// including its title bar and content frames).
|
||||||
|
func (w *Window) Size() render.Rect {
|
||||||
|
return w.body.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize the window.
|
||||||
|
func (w *Window) Resize(size render.Rect) {
|
||||||
|
w.BaseWidget.Resize(size)
|
||||||
|
w.body.Resize(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center the window on screen by providing your screen (app window) size.
|
||||||
|
func (w *Window) Center(width, height int) {
|
||||||
|
w.MoveTo(render.Point{
|
||||||
|
X: (width / 2) - (w.Size().W / 2),
|
||||||
|
Y: (height / 2) - (w.Size().H / 2),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Close the window, hiding it from display and calling its CloseWindow handler.
|
// Close the window, hiding it from display and calling its CloseWindow handler.
|
||||||
func (w *Window) Close() {
|
func (w *Window) Close() {
|
||||||
w.Hide()
|
w.Hide()
|
||||||
|
@ -316,12 +355,12 @@ func (w *Window) Pack(child Widget, config ...Pack) {
|
||||||
|
|
||||||
// Place a child widget into the window's main frame.
|
// Place a child widget into the window's main frame.
|
||||||
func (w *Window) Place(child Widget, config Place) {
|
func (w *Window) Place(child Widget, config Place) {
|
||||||
w.content.Place(child, config)
|
w.body.Place(child, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TitleBar returns the title bar widget.
|
// TitleBar returns the title bar widgets.
|
||||||
func (w *Window) TitleBar() *Frame {
|
func (w *Window) TitleBar() (*Frame, *Label) {
|
||||||
return w.titleBar
|
return w.titleBar, w.titleLabel
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure the widget. Color and style changes are passed down to the inner
|
// Configure the widget. Color and style changes are passed down to the inner
|
||||||
|
@ -331,6 +370,14 @@ func (w *Window) Configure(C Config) {
|
||||||
w.body.Configure(C)
|
w.body.Configure(C)
|
||||||
|
|
||||||
// Don't pass dimensions down any further than the body.
|
// Don't pass dimensions down any further than the body.
|
||||||
|
// TODO: this causes the content frame to compute its size
|
||||||
|
// dynamically based on Packed widgets, but if using Place on
|
||||||
|
// your window, the content frame doesn't know a size by which
|
||||||
|
// to place the child relative to (Frame has size 0x0).
|
||||||
|
// Commenting out these two lines causes windows to render very
|
||||||
|
// incorrectly (child frame content flying off the window bottom).
|
||||||
|
// In the meantime, Window.Place intercepts it and draws it onto
|
||||||
|
// the parent window directly so it works how you expect.
|
||||||
C.Width = 0
|
C.Width = 0
|
||||||
C.Height = 0
|
C.Height = 0
|
||||||
w.content.Configure(C)
|
w.content.Configure(C)
|
||||||
|
@ -341,6 +388,11 @@ func (w *Window) ConfigureTitle(C Config) {
|
||||||
w.titleBar.Configure(C)
|
w.titleBar.Configure(C)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ContentFrame returns the main content Frame of this window.
|
||||||
|
func (w *Window) ContentFrame() *Frame {
|
||||||
|
return w.content
|
||||||
|
}
|
||||||
|
|
||||||
// Compute the window.
|
// Compute the window.
|
||||||
func (w *Window) Compute(e render.Engine) {
|
func (w *Window) Compute(e render.Engine) {
|
||||||
w.engine = e // hang onto it in case of maximize
|
w.engine = e // hang onto it in case of maximize
|
||||||
|
@ -357,3 +409,10 @@ func (w *Window) Present(e render.Engine, P render.Point) {
|
||||||
// Call the BaseWidget Present in case we have subscribers.
|
// Call the BaseWidget Present in case we have subscribers.
|
||||||
w.BaseWidget.Present(e, P)
|
w.BaseWidget.Present(e, P)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Destroy hides the window.
|
||||||
|
func (w *Window) Destroy() {
|
||||||
|
if !w.Hidden() {
|
||||||
|
w.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -66,7 +66,6 @@ func (s *Supervisor) addWindow(win *Window) {
|
||||||
s.winFocus = &FocusedWindow{
|
s.winFocus = &FocusedWindow{
|
||||||
window: win,
|
window: win,
|
||||||
}
|
}
|
||||||
s.winTop = s.winFocus
|
|
||||||
s.winBottom = s.winFocus
|
s.winBottom = s.winFocus
|
||||||
win.SetFocus(true)
|
win.SetFocus(true)
|
||||||
} else {
|
} else {
|
||||||
|
@ -138,7 +137,6 @@ func (s *Supervisor) FocusWindow(win *Window) error {
|
||||||
s.winFocus = target
|
s.winFocus = target
|
||||||
|
|
||||||
// Fix the top and bottom pointers.
|
// Fix the top and bottom pointers.
|
||||||
s.winTop = s.winFocus
|
|
||||||
if newBottom != nil {
|
if newBottom != nil {
|
||||||
s.winBottom = newBottom
|
s.winBottom = newBottom
|
||||||
}
|
}
|
||||||
|
@ -173,12 +171,56 @@ func (s *Supervisor) CloseAllWindows() int {
|
||||||
)
|
)
|
||||||
for node != nil {
|
for node != nil {
|
||||||
i++
|
i++
|
||||||
node.window.Hide()
|
node.window.Close()
|
||||||
node = node.next
|
node = node.next
|
||||||
}
|
}
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloseActiveWindow closes the topmost active window.
|
||||||
|
func (s *Supervisor) CloseActiveWindow() bool {
|
||||||
|
var node = s.winFocus
|
||||||
|
if node != nil {
|
||||||
|
node.window.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the next visible window to focus.
|
||||||
|
for node != nil {
|
||||||
|
if !node.window.Hidden() {
|
||||||
|
s.FocusWindow(node.window)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
node = node.next
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintWindows is a debug function that walks the window tree and prints them to your console.
|
||||||
|
func (s *Supervisor) PrintWindows() {
|
||||||
|
var (
|
||||||
|
node = s.winBottom
|
||||||
|
i int
|
||||||
|
)
|
||||||
|
|
||||||
|
fmt.Println("From the bottom:")
|
||||||
|
for node != nil {
|
||||||
|
i++
|
||||||
|
fmt.Printf("%d. %s focused=%+v hidden=%+v\n", i, node.window, node.window.Focused(), node.window.Hidden())
|
||||||
|
node = node.prev
|
||||||
|
}
|
||||||
|
|
||||||
|
node = s.winFocus
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
fmt.Println("Focus order:")
|
||||||
|
for node != nil {
|
||||||
|
i++
|
||||||
|
fmt.Printf("%d. %s focused=%+v hidden=%+v\n", i, node.window, node.window.Focused(), node.window.Hidden())
|
||||||
|
node = node.next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// presentWindows draws the windows from bottom to top.
|
// presentWindows draws the windows from bottom to top.
|
||||||
func (s *Supervisor) presentWindows(e render.Engine) {
|
func (s *Supervisor) presentWindows(e render.Engine) {
|
||||||
item := s.winBottom
|
item := s.winBottom
|
||||||
|
|