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 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.)_
|
||||
|
||||
It is very much a **work in progress** and may contain bugs and its API may
|
||||
change as bugs are fixed or features added.
|
||||
|
||||
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
|
||||
the SDL2 and Canvas back-ends.
|
||||
(GitHub mirror: [kirsle/render](https://github.com/kirsle/render))
|
||||
|
||||
**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
|
||||
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.
|
||||
|
||||
# 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
|
||||
package main
|
||||
|
||||
|
@ -97,19 +100,22 @@ most complex.
|
|||
|
||||
**Fully implemented widgets:**
|
||||
|
||||
In order of simplicity:
|
||||
|
||||
* [x] **BaseWidget**: the base class of all Widgets.
|
||||
* The `Widget` interface describes the functions common to all Widgets,
|
||||
such as SetBackground, Configure, MoveTo, Resize, and so on.
|
||||
* BaseWidget provides sane default implementations for all the methods
|
||||
required by the Widget interface. Most Widgets inherit from
|
||||
the BaseWidget.
|
||||
* [x] **Frame**: a layout wrapper for other widgets.
|
||||
the BaseWidget and override what they need.
|
||||
* [x] **Frame**: a layout wrapper for child widgets.
|
||||
* Pack() lets you add child widgets to the Frame, aligned against one side
|
||||
or another, and ability to expand widgets to take up remaining space in
|
||||
their part of the Frame.
|
||||
* 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
|
||||
aligned to the Center (horizontal) or Middle (vertical) of the parent.
|
||||
[Example](eg/frame-place)
|
||||
* [x] **Label**: Textual labels for your UI.
|
||||
* Supports TrueType fonts, color, stroke, drop shadow, font size, etc.
|
||||
* 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
|
||||
label next to a small check button. Clicking the label will toggle the
|
||||
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.
|
||||
* Can be managed by Supervisor to give Window Manager controls to it
|
||||
(drag it by its title bar, Close button, window focus, multiple overlapping
|
||||
windows, and so on).
|
||||
* [x] **Tooltip**: a mouse hover label attached to a widget.
|
||||
windows, and so on). [Example](eg/windows)
|
||||
* [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] **MenuBar**: a specialized Frame that groups a bunch of MenuButtons and
|
||||
provides a simple API to add menus and items to it.
|
||||
* [x] **Menu**: a frame full of clickable links and separators. Usually used as
|
||||
a modal pop-up by the MenuButton and MenuBar.
|
||||
|
||||
**Work in progress widgets:**
|
||||
|
||||
* [ ] **Scrollbar**: a Frame including a trough, scroll buttons and a
|
||||
a modal pop-up by the MenuButton and MenuBar. [Example](eg/menus)
|
||||
* [x] **SelectBox**: a kind of MenuButton that lets the user choose a
|
||||
value from a list of possible values.
|
||||
* [x] **Scrollbar**: a Frame including a trough, scroll buttons and a
|
||||
draggable slider.
|
||||
* [ ] **SelectBox:** a kind of MenuButton that lets the user choose a value
|
||||
from a list of possible values, bound to a string variable.
|
||||
* [x] **ListBox**: a multi-line select box with a ScrollBar that can hold arbitrary
|
||||
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
|
||||
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
|
||||
|
||||
|
|
62
button.go
|
@ -5,13 +5,19 @@ import (
|
|||
"fmt"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/ui/theme"
|
||||
"git.kirsle.net/go/ui/style"
|
||||
)
|
||||
|
||||
// Button is a clickable button.
|
||||
type Button struct {
|
||||
BaseWidget
|
||||
Name string
|
||||
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.
|
||||
hovering bool
|
||||
|
@ -21,28 +27,34 @@ type Button struct {
|
|||
// NewButton creates a new Button.
|
||||
func NewButton(name string, child Widget) *Button {
|
||||
w := &Button{
|
||||
Name: name,
|
||||
child: child,
|
||||
style: &style.DefaultButton,
|
||||
}
|
||||
w.IDFunc(func() string {
|
||||
return fmt.Sprintf("Button<%s>", name)
|
||||
return fmt.Sprintf("Button<%s>", w.Name)
|
||||
})
|
||||
|
||||
w.Configure(Config{
|
||||
BorderSize: 2,
|
||||
BorderStyle: BorderRaised,
|
||||
OutlineSize: 1,
|
||||
OutlineColor: theme.ButtonOutlineColor,
|
||||
Background: theme.ButtonBackgroundColor,
|
||||
})
|
||||
w.SetStyle(Theme.Button)
|
||||
|
||||
w.Handle(MouseOver, func(e EventData) error {
|
||||
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
|
||||
})
|
||||
w.Handle(MouseOut, func(e EventData) error {
|
||||
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
|
||||
})
|
||||
|
||||
|
@ -53,13 +65,39 @@ func NewButton(name string, child Widget) *Button {
|
|||
})
|
||||
w.Handle(MouseUp, func(e EventData) error {
|
||||
w.clicked = false
|
||||
w.SetBorderStyle(BorderRaised)
|
||||
w.SetBorderStyle(BorderStyle(w.style.BorderStyle))
|
||||
return nil
|
||||
})
|
||||
|
||||
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.
|
||||
func (w *Button) Children() []Widget {
|
||||
return []Widget{w.child}
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"strconv"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/ui/theme"
|
||||
)
|
||||
|
||||
// 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)
|
||||
})
|
||||
|
||||
w.SetStyle(Theme.Button)
|
||||
|
||||
w.setup()
|
||||
return w
|
||||
}
|
||||
|
@ -42,6 +43,9 @@ func NewRadioButton(name string, stringVar *string, value string, child Widget)
|
|||
w.IDFunc(func() string {
|
||||
return fmt.Sprintf(`RadioButton<%s "%s" %s>`, name, w.Value, strconv.FormatBool(*w.StringVar == w.Value))
|
||||
})
|
||||
|
||||
w.SetStyle(Theme.Button)
|
||||
|
||||
w.setup()
|
||||
return w
|
||||
}
|
||||
|
@ -57,16 +61,28 @@ func (w *CheckButton) Compute(e render.Engine) {
|
|||
} else {
|
||||
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)
|
||||
}
|
||||
|
||||
// setup the common things between checkboxes and radioboxes.
|
||||
func (w *CheckButton) setup() {
|
||||
var borderStyle BorderStyle = BorderRaised
|
||||
var (
|
||||
borderStyle BorderStyle = BorderRaised
|
||||
background = w.style.Background
|
||||
)
|
||||
if w.BoolVar != nil {
|
||||
if *w.BoolVar == true {
|
||||
borderStyle = BorderSunken
|
||||
background = w.style.Background.Darken(40)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,18 +90,31 @@ func (w *CheckButton) setup() {
|
|||
BorderSize: 2,
|
||||
BorderStyle: borderStyle,
|
||||
OutlineSize: 1,
|
||||
OutlineColor: theme.ButtonOutlineColor,
|
||||
Background: theme.ButtonBackgroundColor,
|
||||
OutlineColor: w.style.OutlineColor,
|
||||
Background: background,
|
||||
})
|
||||
|
||||
w.Handle(MouseOver, func(ed EventData) error {
|
||||
w.hovering = true
|
||||
w.SetBackground(theme.ButtonHoverColor)
|
||||
w.SetBackground(w.style.HoverBackground)
|
||||
return nil
|
||||
})
|
||||
w.Handle(MouseOut, func(ed EventData) error {
|
||||
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
|
||||
})
|
||||
|
||||
|
@ -115,9 +144,12 @@ func (w *CheckButton) setup() {
|
|||
|
||||
if sunken {
|
||||
w.SetBorderStyle(BorderSunken)
|
||||
w.SetBackground(w.style.Background.Darken(40))
|
||||
} else {
|
||||
w.SetBorderStyle(BorderRaised)
|
||||
w.SetBackground(w.style.Background)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
21
checkbox.go
|
@ -1,5 +1,7 @@
|
|||
package ui
|
||||
|
||||
import "errors"
|
||||
|
||||
// Checkbox combines a CheckButton with a widget like a Label.
|
||||
type Checkbox struct {
|
||||
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 {
|
||||
// Our custom checkbutton widget.
|
||||
mark := NewFrame(name + "_mark")
|
||||
mark.Configure(Config{
|
||||
Width: 6,
|
||||
Height: 6,
|
||||
})
|
||||
|
||||
w := &Checkbox{
|
||||
child: child,
|
||||
|
@ -33,7 +39,7 @@ func makeCheckbox(name string, boolVar *bool, stringVar *string, value string, c
|
|||
w.Frame.Setup()
|
||||
|
||||
// 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) {
|
||||
w.child.Handle(e, func(ed EventData) error {
|
||||
return w.button.Event(e, ed)
|
||||
|
@ -56,6 +62,19 @@ func (w *Checkbox) Child() Widget {
|
|||
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.
|
||||
func (w *Checkbox) Supervise(s *Supervisor) {
|
||||
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 = `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
|
||||
aiCrcAAAAABJRU5ErkJggg==`
|
19
eg/README.md
|
@ -1,10 +1,15 @@
|
|||
# Examples for go/ui
|
||||
|
||||
* [Hello, World!](hello-world/): a basic UI demo.
|
||||
* [Frame Place()](frame-place/): demonstrates using the Place() layout management
|
||||
option for Frame widgets.]
|
||||
* [Window Manager](windows/): demonstrates the Window widget and window
|
||||
management features of the Supervisor.
|
||||
* [Tooltip](tooltip/): demonstrates the Tooltip widget on a variety of buttons
|
||||
scattered around the window.
|
||||
Here are some example programs using go/ui, each accompanied by a
|
||||
screenshot and description:
|
||||
|
||||
* [Hello, World!](hello-world/): a basic UI demo with a Label and a Button.
|
||||
* [Frame Placement](frame-place/): demonstrates using the Place() layout management option for Frame widgets.
|
||||
* [Window Manager](windows/): demonstrates the Window widget and window management features of the Supervisor.
|
||||
* [Forms](forms/): demonstrates some form controls and the `magicform` helper module for building forms quickly.
|
||||
* [Tooltip](tooltip/): demonstrates the Tooltip widget on a variety of buttons scattered around the window.
|
||||
* [Menus](menus/): demonstrates various Menu Buttons and a Menu Bar.
|
||||
* [Themes](themes/): a UI demo that shows off the Default, Flat, and Dark UI themes as part of experimental theming support.
|
||||
* [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.
|
||||
button.Handle(ui.Click, func(ed ui.EventData) {
|
||||
button.Handle(ui.Click, func(ed ui.EventData) error {
|
||||
window.SetTitle(parent.Name + ": " + setting.Label)
|
||||
return nil
|
||||
})
|
||||
|
||||
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,
|
||||
},
|
||||
}))
|
||||
button.Handle(ui.Click, func(ed ui.EventData) {
|
||||
button.Handle(ui.Click, func(ed ui.EventData) error {
|
||||
fmt.Println("I've been clicked!")
|
||||
return nil
|
||||
})
|
||||
mw.Pack(button, ui.Pack{
|
||||
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
|
||||
|
||||
![Screenshot](screenshot.png)
|
||||
|
||||
This example shows off the Menu, MenuButton, and MenuBar widgets.
|
||||
|
||||
* 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.
|
||||
button.Handle(ui.Click, func(ed ui.EventData) {
|
||||
button.Handle(ui.Click, func(ed ui.EventData) error {
|
||||
window.SetTitle(parent.Name + ": " + setting.Label)
|
||||
return nil
|
||||
})
|
||||
|
||||
// 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
|
||||
|
||||
// Widget placement settings.
|
||||
packs map[Side][]packedWidget // Packed widgets
|
||||
placed []placedWidget // Placed widgets
|
||||
packs map[Side][]*packedWidget // Packed widgets
|
||||
placed []*placedWidget // Placed widgets
|
||||
widgets []Widget
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ type Frame struct {
|
|||
func NewFrame(name string) *Frame {
|
||||
w := &Frame{
|
||||
Name: name,
|
||||
packs: map[Side][]packedWidget{},
|
||||
packs: map[Side][]*packedWidget{},
|
||||
widgets: []Widget{},
|
||||
}
|
||||
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.
|
||||
func (w *Frame) Setup() {
|
||||
if w.packs == nil {
|
||||
w.packs = map[Side][]packedWidget{}
|
||||
w.packs = map[Side][]*packedWidget{}
|
||||
}
|
||||
if w.widgets == nil {
|
||||
w.widgets = []Widget{}
|
||||
|
@ -99,7 +99,10 @@ func (w *Frame) Present(e render.Engine, P render.Point) {
|
|||
|
||||
// Draw the widgets.
|
||||
for _, child := range w.widgets {
|
||||
// child.Compute(e)
|
||||
if child.Hidden() {
|
||||
continue
|
||||
}
|
||||
|
||||
p := child.Point()
|
||||
moveTo := render.NewPoint(
|
||||
P.X+p.X+w.BoxThickness(1),
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
)
|
||||
|
||||
|
@ -29,9 +31,17 @@ func (w *Frame) Pack(child Widget, config ...Pack) {
|
|||
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?
|
||||
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
|
||||
|
@ -49,13 +59,41 @@ func (w *Frame) Pack(child Widget, config ...Pack) {
|
|||
// Adopt the child widget so it can access the Frame.
|
||||
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,
|
||||
pack: C,
|
||||
})
|
||||
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.
|
||||
func (w *Frame) computePacked(e render.Engine) {
|
||||
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.
|
||||
maxWidth int
|
||||
maxHeight int
|
||||
visited = []packedWidget{}
|
||||
expanded = []packedWidget{}
|
||||
visited = []*packedWidget{}
|
||||
expanded = []*packedWidget{}
|
||||
)
|
||||
|
||||
// Iterate through all directions and compute how much space to
|
||||
|
|
|
@ -37,9 +37,18 @@ type placedWidget struct {
|
|||
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) {
|
||||
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,
|
||||
place: config,
|
||||
})
|
||||
|
@ -54,6 +63,8 @@ func (w *Frame) Place(child Widget, config Place) {
|
|||
func (w *Frame) computePlaced(e render.Engine) {
|
||||
var (
|
||||
frameSize = w.BoxSize()
|
||||
// maxWidth int
|
||||
// maxHeight int
|
||||
)
|
||||
|
||||
for _, row := range w.placed {
|
||||
|
@ -87,6 +98,7 @@ func (w *Frame) computePlaced(e render.Engine) {
|
|||
if row.place.Middle {
|
||||
moveTo.Y = frameSize.H - (w.Size().H / 2) - (row.widget.Size().H / 2)
|
||||
}
|
||||
|
||||
row.widget.MoveTo(moveTo)
|
||||
row.widget.Compute(e)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ func AbsolutePosition(w Widget) render.Point {
|
|||
var (
|
||||
node = w
|
||||
ok bool
|
||||
pt render.Point
|
||||
)
|
||||
|
||||
for {
|
||||
|
@ -20,7 +21,9 @@ func AbsolutePosition(w Widget) render.Point {
|
|||
return abs
|
||||
}
|
||||
|
||||
abs.Add(node.Point())
|
||||
pt = node.Point()
|
||||
pt.Add(render.NewPoint(node.BorderSize(), node.BorderSize()))
|
||||
abs.Add(pt)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
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-20200102014411-4d008b5c468d
|
||||
require (
|
||||
git.kirsle.net/go/render v0.0.0-20220505053906-129a24300dfa
|
||||
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-20200102014411-4d008b5c468d/go.mod h1:ywZtC+zE2SpeObfkw0OvG01pWHQadsVQ4WDKOYzaejc=
|
||||
github.com/veandco/go-sdl2 v0.4.1 h1:HmSBvVmKWI8LAOeCfTTM8R33rMyPcs6U3o8n325c9Qg=
|
||||
git.kirsle.net/go/render v0.0.0-20220505053906-129a24300dfa h1:Oa99SXkmFGnUNy+toPMQyW/eYotN1nZ9BWAThQ/huiM=
|
||||
git.kirsle.net/go/render v0.0.0-20220505053906-129a24300dfa/go.mod h1:ss7pvZbGWrMaDuZwyUTjV9+T0AJwAkxZZHwMFsvHrkk=
|
||||
github.com/veandco/go-sdl2 v0.4.1/go.mod h1:FB+kTpX9YTE+urhYiClnRzpOXbiWgaU3+5F2AB78DPg=
|
||||
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.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.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"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
"golang.org/x/image/bmp"
|
||||
)
|
||||
|
||||
// ImageType for supported image formats.
|
||||
|
@ -27,8 +29,8 @@ type Image struct {
|
|||
|
||||
// Configurable fields for the constructor.
|
||||
Type ImageType
|
||||
Image image.Image
|
||||
texture render.Texturer
|
||||
Image image.Image // a Go image version
|
||||
texture render.Texturer // (SDL2) Texture, lazy inited on Present.
|
||||
}
|
||||
|
||||
// NewImage creates a new Image.
|
||||
|
@ -46,15 +48,24 @@ func NewImage(c Image) *Image {
|
|||
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.
|
||||
func ImageFromTexture(tex render.Texturer) *Image {
|
||||
return &Image{
|
||||
texture: tex,
|
||||
Image: tex.Image(),
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -65,17 +76,24 @@ func ImageFromFile(e render.Engine, filename string) (*Image, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
tex, err := e.StoreTexture(filename, img)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Image{
|
||||
Image: img,
|
||||
texture: tex,
|
||||
Image: img,
|
||||
}, 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.
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
|
||||
tex, err := e.LoadTexture(filename)
|
||||
// Open the file from disk.
|
||||
fh, err := os.Open(filename)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -116,16 +156,39 @@ func (w *Image) GetRGBA() *image.RGBA {
|
|||
return rgba
|
||||
}
|
||||
|
||||
// Compute the widget.
|
||||
func (w *Image) Compute(e render.Engine) {
|
||||
w.Resize(w.texture.Size())
|
||||
|
||||
// Call the BaseWidget Compute in case we have subscribers.
|
||||
w.BaseWidget.Compute(e)
|
||||
// Size returns the dimensions of the image which is also the widget's size.
|
||||
func (w *Image) Size() render.Rect {
|
||||
if w.Image != nil {
|
||||
bounds := w.Image.Bounds().Canon()
|
||||
return render.Rect{
|
||||
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) {
|
||||
// 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()
|
||||
dst := render.Rect{
|
||||
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.
|
||||
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"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/ui/style"
|
||||
)
|
||||
|
||||
// DefaultFont is the default font settings used for a Label.
|
||||
|
@ -23,6 +24,7 @@ type Label struct {
|
|||
IntVariable *int
|
||||
Font render.Text
|
||||
|
||||
style *style.Label
|
||||
width int
|
||||
height int
|
||||
lineHeight int
|
||||
|
@ -36,6 +38,7 @@ func NewLabel(c Label) *Label {
|
|||
IntVariable: c.IntVariable,
|
||||
Font: DefaultFont,
|
||||
}
|
||||
w.SetStyle(Theme.Label)
|
||||
if !c.Font.IsZero() {
|
||||
w.Font = c.Font
|
||||
}
|
||||
|
@ -45,6 +48,17 @@ func NewLabel(c Label) *Label {
|
|||
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
|
||||
// available or else the Text attribute instead.
|
||||
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) {
|
||||
var (
|
||||
// Position and size of the MenuButton button.
|
||||
buttonPoint = w.Point()
|
||||
buttonPoint = AbsolutePosition(w)
|
||||
buttonSize = w.Size()
|
||||
|
||||
// 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 (
|
||||
NullEvent Event = iota
|
||||
MouseOver
|
||||
MouseMove
|
||||
MouseOut
|
||||
MouseDown
|
||||
MouseUp
|
||||
|
@ -22,6 +23,7 @@ const (
|
|||
KeyDown
|
||||
KeyUp
|
||||
KeyPress
|
||||
Scroll
|
||||
|
||||
// Drag/drop event handlers.
|
||||
DragStop // if a widget is being dragged and the drag is done
|
||||
|
@ -37,6 +39,9 @@ const (
|
|||
// Lifecycle event handlers.
|
||||
Compute // fired whenever the widget runs Compute
|
||||
Present // fired whenever the widget runs Present
|
||||
|
||||
// Form field events.
|
||||
Change
|
||||
)
|
||||
|
||||
// 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 *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
|
||||
|
@ -67,8 +105,10 @@ type Supervisor struct {
|
|||
|
||||
// List of window focus history for Window Manager.
|
||||
winFocus *FocusedWindow
|
||||
winTop *FocusedWindow // pointer to top-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.
|
||||
|
@ -84,6 +124,7 @@ func NewSupervisor() *Supervisor {
|
|||
hovering: map[int]interface{}{},
|
||||
clicked: map[int]bool{},
|
||||
modals: []Widget{},
|
||||
onTop: []Widget{},
|
||||
dd: NewDragDrop(),
|
||||
}
|
||||
}
|
||||
|
@ -147,7 +188,8 @@ func (s *Supervisor) Loop(ev *event.State) error {
|
|||
// The mouse has been released. TODO: make mouse button important?
|
||||
for _, child := range hovering {
|
||||
child.widget.Event(Drop, EventData{
|
||||
Point: XY,
|
||||
Widget: child.widget,
|
||||
Point: XY,
|
||||
})
|
||||
}
|
||||
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 target := s.dd.Widget(); target != nil {
|
||||
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 {
|
||||
// A widget in the active window has accepted an event. Do not pass
|
||||
// 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.
|
||||
//
|
||||
// This function has two use cases:
|
||||
// - In runWindowEvents where we run events for the top-most focused window of
|
||||
// the window manager.
|
||||
// - In Supervisor.Loop() for the widgets that are NOT owned by a managed
|
||||
// window, so that these widgets always get events.
|
||||
// - In runWindowEvents where we run events for the top-most focused window of
|
||||
// the window manager.
|
||||
// - In Supervisor.Loop() for the widgets that are NOT owned by a managed
|
||||
// window, so that these widgets always get events.
|
||||
//
|
||||
// Parameters:
|
||||
// XY (Point): mouse cursor position as calculated in Loop()
|
||||
// ev, hovering, outside: values from Loop(), self explanatory.
|
||||
// behavior: indicates how this method is being used.
|
||||
//
|
||||
// XY (Point): mouse cursor position as calculated in Loop()
|
||||
// ev, hovering, outside: values from Loop(), self explanatory.
|
||||
// behavior: indicates how this method is being used.
|
||||
//
|
||||
// behavior options:
|
||||
// 0: widgets NOT part of a managed window. On this pass, if a widget IS
|
||||
// a part of a window, it gets no events triggered.
|
||||
// 1: widgets are part of the active focused window.
|
||||
//
|
||||
// 0: widgets NOT part of a managed window. On this pass, if a widget IS
|
||||
// a part of a window, it gets no events triggered.
|
||||
// 1: widgets are part of the active focused window.
|
||||
func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State,
|
||||
hovering, outside []WidgetSlot, toFocusedWindow bool) (bool, error) {
|
||||
// Do we run any events?
|
||||
|
@ -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 window and activating widgets/other windows behind it.
|
||||
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.
|
||||
if XY.Inside(AbsoluteRect(s.winFocus.window)) {
|
||||
cursorInsideFocusedWindow = true
|
||||
|
@ -343,29 +388,42 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State,
|
|||
// Cursor has intersected the widget.
|
||||
if _, ok := s.hovering[id]; !ok {
|
||||
handle(w.Event(MouseOver, EventData{
|
||||
Point: XY,
|
||||
Widget: w,
|
||||
Point: XY,
|
||||
}))
|
||||
s.hovering[id] = nil
|
||||
}
|
||||
|
||||
isClicked, _ := s.clicked[id]
|
||||
isClicked := s.clicked[id]
|
||||
if ev.Button1 {
|
||||
if !isClicked {
|
||||
err := w.Event(MouseDown, EventData{
|
||||
Point: XY,
|
||||
Widget: w,
|
||||
Point: XY,
|
||||
})
|
||||
handle(err)
|
||||
s.clicked[id] = true
|
||||
}
|
||||
} else if isClicked {
|
||||
handle(w.Event(MouseUp, EventData{
|
||||
Point: XY,
|
||||
Widget: w,
|
||||
Point: XY,
|
||||
}))
|
||||
handle(w.Event(Click, EventData{
|
||||
Point: XY,
|
||||
Widget: w,
|
||||
Point: XY,
|
||||
}))
|
||||
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 {
|
||||
var (
|
||||
|
@ -384,14 +442,16 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State,
|
|||
// Cursor is not intersecting the widget.
|
||||
if _, ok := s.hovering[id]; ok {
|
||||
handle(w.Event(MouseOut, EventData{
|
||||
Point: XY,
|
||||
Widget: w,
|
||||
Point: XY,
|
||||
}))
|
||||
delete(s.hovering, id)
|
||||
}
|
||||
|
||||
if _, ok := s.clicked[id]; ok {
|
||||
handle(w.Event(MouseUp, EventData{
|
||||
Point: XY,
|
||||
Widget: w,
|
||||
Point: XY,
|
||||
}))
|
||||
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 stopPropagation {
|
||||
return ranEvents, ErrStopPropagation
|
||||
|
@ -451,6 +517,16 @@ func (s *Supervisor) Present(e render.Engine) {
|
|||
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
|
||||
|
@ -510,3 +586,33 @@ func (s *Supervisor) PopModal(w Widget) bool {
|
|||
|
||||
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
|
||||
|
||||
import "git.kirsle.net/go/render"
|
||||
import (
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/ui/style"
|
||||
)
|
||||
|
||||
// Color schemes.
|
||||
var (
|
||||
ButtonBackgroundColor = render.RGBA(200, 200, 200, 255)
|
||||
ButtonHoverColor = render.RGBA(200, 255, 255, 255)
|
||||
ButtonOutlineColor = render.Black
|
||||
InputBackgroundColor = render.White
|
||||
|
||||
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"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/ui/style"
|
||||
)
|
||||
|
||||
func init() {
|
||||
precomputeArrows()
|
||||
}
|
||||
|
||||
// Tooltip attaches a mouse-over popup to another widget.
|
||||
/*
|
||||
Tooltip attaches a mouse-over popup to another widget.
|
||||
|
||||
|
||||
*/
|
||||
type Tooltip struct {
|
||||
BaseWidget
|
||||
|
||||
|
@ -19,7 +24,9 @@ type Tooltip struct {
|
|||
Text string // Text to show in the tooltip.
|
||||
TextVariable *string // String pointer instead of text.
|
||||
Edge Edge // side to display tooltip on
|
||||
supervisor *Supervisor
|
||||
|
||||
style *style.Tooltip
|
||||
target Widget
|
||||
lineHeight int
|
||||
font render.Text
|
||||
|
@ -66,7 +73,9 @@ func NewTooltip(target Widget, tt Tooltip) *Tooltip {
|
|||
return nil
|
||||
})
|
||||
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
|
||||
})
|
||||
|
||||
|
@ -74,9 +83,40 @@ func NewTooltip(target Widget, tt Tooltip) *Tooltip {
|
|||
return fmt.Sprintf(`Tooltip<"%s">`, w.Value())
|
||||
})
|
||||
|
||||
w.SetStyle(Theme.Tooltip)
|
||||
|
||||
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
|
||||
// configured Text or the TextVariable pointer.
|
||||
func (w *Tooltip) Value() string {
|
||||
|
|
|
@ -21,10 +21,24 @@ func ExampleTooltip() {
|
|||
// Add a tooltip to it. The tooltip attaches itself to the button's
|
||||
// MouseOver, MouseOut, Compute and Present handlers -- you don't need to
|
||||
// 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!",
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -75,6 +75,9 @@ type Widget interface {
|
|||
|
||||
// Render the final widget onto the drawing engine.
|
||||
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.
|
||||
|
@ -523,3 +526,6 @@ func (w *BaseWidget) Handle(event Event, fn func(EventData) error) {
|
|||
|
||||
// OnMouseOut should be overridden on widgets who want this event.
|
||||
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"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/ui/style"
|
||||
)
|
||||
|
||||
// Window is a frame with a title bar.
|
||||
|
@ -19,6 +20,7 @@ type Window struct {
|
|||
InactiveTitleForeground render.Color
|
||||
|
||||
// Private widgets.
|
||||
style *style.Window
|
||||
body *Frame
|
||||
titleBar *Frame
|
||||
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.
|
||||
titleBar, titleLabel := w.setupTitleBar()
|
||||
w.body.Pack(titleBar, Pack{
|
||||
|
@ -87,9 +83,32 @@ func NewWindow(title string) *Window {
|
|||
// Set up parent/child relationships
|
||||
w.body.SetParent(w)
|
||||
|
||||
w.SetStyle(Theme.Window)
|
||||
|
||||
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.
|
||||
func (w *Window) setupTitleBar() (*Frame, *Label) {
|
||||
frame := NewFrame("Titlebar for Window: " + w.Title)
|
||||
|
@ -102,7 +121,7 @@ func (w *Window) setupTitleBar() (*Frame, *Label) {
|
|||
TextVariable: &w.Title,
|
||||
Font: render.Text{
|
||||
Color: w.ActiveTitleForeground,
|
||||
Size: 10,
|
||||
Size: 11,
|
||||
Stroke: w.ActiveTitleBackground.Darken(40),
|
||||
Padding: 2,
|
||||
},
|
||||
|
@ -260,12 +279,12 @@ func (w *Window) SetFocus(v bool) {
|
|||
|
||||
// Update the title bar colors.
|
||||
var (
|
||||
bg = w.ActiveTitleBackground
|
||||
fg = w.ActiveTitleForeground
|
||||
bg = w.style.ActiveTitleBackground
|
||||
fg = w.style.ActiveTitleForeground
|
||||
)
|
||||
if !w.focused {
|
||||
bg = w.InactiveTitleBackground
|
||||
fg = w.InactiveTitleForeground
|
||||
bg = w.style.InactiveTitleBackground
|
||||
fg = w.style.InactiveTitleForeground
|
||||
}
|
||||
w.titleBar.SetBackground(bg)
|
||||
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.
|
||||
func (w *Window) Close() {
|
||||
w.Hide()
|
||||
|
@ -316,12 +355,12 @@ func (w *Window) Pack(child Widget, config ...Pack) {
|
|||
|
||||
// Place a child widget into the window's main frame.
|
||||
func (w *Window) Place(child Widget, config Place) {
|
||||
w.content.Place(child, config)
|
||||
w.body.Place(child, config)
|
||||
}
|
||||
|
||||
// TitleBar returns the title bar widget.
|
||||
func (w *Window) TitleBar() *Frame {
|
||||
return w.titleBar
|
||||
// TitleBar returns the title bar widgets.
|
||||
func (w *Window) TitleBar() (*Frame, *Label) {
|
||||
return w.titleBar, w.titleLabel
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// 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.Height = 0
|
||||
w.content.Configure(C)
|
||||
|
@ -341,6 +388,11 @@ func (w *Window) ConfigureTitle(C Config) {
|
|||
w.titleBar.Configure(C)
|
||||
}
|
||||
|
||||
// ContentFrame returns the main content Frame of this window.
|
||||
func (w *Window) ContentFrame() *Frame {
|
||||
return w.content
|
||||
}
|
||||
|
||||
// Compute the window.
|
||||
func (w *Window) Compute(e render.Engine) {
|
||||
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.
|
||||
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{
|
||||
window: win,
|
||||
}
|
||||
s.winTop = s.winFocus
|
||||
s.winBottom = s.winFocus
|
||||
win.SetFocus(true)
|
||||
} else {
|
||||
|
@ -138,7 +137,6 @@ func (s *Supervisor) FocusWindow(win *Window) error {
|
|||
s.winFocus = target
|
||||
|
||||
// Fix the top and bottom pointers.
|
||||
s.winTop = s.winFocus
|
||||
if newBottom != nil {
|
||||
s.winBottom = newBottom
|
||||
}
|
||||
|
@ -173,12 +171,56 @@ func (s *Supervisor) CloseAllWindows() int {
|
|||
)
|
||||
for node != nil {
|
||||
i++
|
||||
node.window.Hide()
|
||||
node.window.Close()
|
||||
node = node.next
|
||||
}
|
||||
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.
|
||||
func (s *Supervisor) presentWindows(e render.Engine) {
|
||||
item := s.winBottom
|
||||
|
|