Compare commits

...

34 Commits

Author SHA1 Message Date
Noah 20a9d7bdff Update Window.Destroy to only hide window if visible 2023-12-09 15:00:25 -08:00
Noah e912e2bd03 Go module update 2023-12-08 19:54:43 -08:00
Noah 28280f08bd
Merge pull request #1 from SketchyMaze/dependabot/go_modules/golang.org/x/image-0.5.0
Bump golang.org/x/image from 0.0.0-20211028202545-6944b10bf410 to 0.5.0
2023-12-08 19:54:08 -08:00
Noah 14bd7446ef
Merge branch 'master' into dependabot/go_modules/golang.org/x/image-0.5.0 2023-12-08 19:54:00 -08:00
Noah 98dfa2cce5 Image: add ReplaceFromImage and Destroy to cleanup textures 2023-12-08 19:50:24 -08:00
Noah 8716c479e9 ListBox, ScrollBar, Magic Form and forms demo 2023-04-08 21:18:24 -07:00
dependabot[bot] 43b95a9232
Bump golang.org/x/image from 0.0.0-20211028202545-6944b10bf410 to 0.5.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.0.0-20211028202545-6944b10bf410 to 0.5.0.
- [Release notes](https://github.com/golang/image/releases)
- [Commits](https://github.com/golang/image/commits/v0.5.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-07 00:10:29 +00:00
Noah d82ef0b751 Checkbox: SetText and better BoolVar watch for CheckButton 2022-10-09 17:41:59 -07:00
Noah c99e79d9b0 Pack() or Place() multiple times updates the config 2022-09-24 18:44:45 -07:00
Noah 3b653e503c Window manager: call Close() instead of Hide() so that WindowClose event handlers fire always 2022-04-09 14:19:20 -07:00
Noah c9c7b33647 Tooltips: how to draw on top of all widgets
By default, Tooltips will present after their associated widget presents
(if the mouse cursor is hovering over that widget, and the tooltip
should appear). But the tooltip is not guaranteed to draw "on top" of
neighboring doodads, unless you choose your Edge carefully depending on
the order you're drawing your widgets.

To solve this, Tooltips can be supervised to DrawOnTop() when they're
activated. To opt in, you simply call the Tooltip.Supervise() function
with your supervisor.
2022-03-05 22:41:20 -08:00
Noah 76ddda352d Bugfix: Window focus linked list
It used to be possible to confuse the window manager in the
CloseActiveWindow() function and the linked list got all broken. Seems
more reliable now! Added function PrintWindows() to inspect the linked
lists.
2022-02-19 20:14:52 -08:00
Noah 49b5cfd037 Cleanup debug code 2022-01-01 18:49:25 -08:00
Noah fee6e1e105 New widget: ColorPicker, plus other changes
New properties are added to EventData for Supervisor events:

* Widget: a reference to the widget which is receiving the event.
* Clicked (bool): for MouseMove events records if the primary button is pressed.
* func RelativePoint(): returns a version of EventData.Point adjusted to be
  relative to the Widget (0,0 at the Widget's absolute position on screen).

Other changes:

* Destroy() method for the Widget interface: widgets that need to free up resources
  on teardown should define this, the BaseWidget provides a no-op implementation.
* Window.Resize() will properly resize a Window.
* Window.Center(w, h int) to easily center a window on screen.
2022-01-01 18:43:36 -08:00
Noah 0a6054baa6 TabFrame: SetTabsHidden Option
SetTabsHidden(bool) can hide the tab row on a TabFrame, leaving only the
content frames behind. This way the caller can provide the transition
changes via SetTab and use the TabFrame only for its management
features.
2021-12-26 20:52:13 -08:00
Noah 79210ae8c9 Add Supervisor.CloseActiveWindow() 2021-10-06 22:19:43 -07:00
Noah 49992aad2a Change URL of GitHub mirror. 2021-10-06 19:50:00 -07:00
Noah e8d4e7008b MouseMove event and increase window title font size 2021-10-03 21:46:36 -07:00
Noah f6703bf1ba Height fix in TabFrame 2021-09-03 20:51:05 -07:00
Noah 5d16f5d50c New Widget: TabFrame
* Added the TabFrame widget with an example program and screenshot
* Button: FixedColor=true to set a consistent background color and not
  worry about it with mouseover/down events.
2021-07-25 20:53:09 -07:00
Noah e7e8b4b2c1 Image: defer SDL2 texture loading until Present 2021-07-19 21:23:55 -07:00
Noah b87b4825af Checkbox updates, SelectBox images 2021-06-13 19:59:22 -07:00
Noah 9a25ec3782 SelectBox: Ability to set the current value 2021-06-06 14:18:43 -07:00
Noah 6df7bade48 Add SelectBox Widget 2021-06-06 13:44:05 -07:00
Noah 8f91971b62 Fix MenuButton location for dropdown menu 2021-06-03 19:09:40 -07:00
Noah Petherbridge ff76b831ad Update example apps and documentation 2021-06-03 12:27:48 -07:00
Noah b7190fe958 Frame: do not draw hidden widgets 2021-01-03 17:05:04 -08:00
Noah c32601391a Add MaxPageButtons to the Pager widget 2020-11-19 20:58:41 -08:00
Noah 8433b1b216 Add Size() method to ui.Window that returns the window body size 2020-11-15 18:07:06 -08:00
Noah e2a561fbd0 Add the Pager Widget 2020-07-09 19:31:46 -07:00
Noah 0e027a9fee CheckButton Styles, Supervisor fixes 2020-07-09 19:29:44 -07:00
Noah e675ead0ed WIP Themes Support 2020-06-17 18:04:18 -07:00
Noah 4206330398 Add Supervisor.GetModal() function 2020-06-04 21:58:45 -07:00
Noah 53c0fed7be Merge pull request 'Menus and Menu Bars' (#2) from menus into master 2020-06-05 02:49:26 +00:00
59 changed files with 4345 additions and 148 deletions

View File

@ -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

View File

@ -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}

View File

@ -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
})
}

View File

@ -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)

584
colorpicker.go Normal file
View File

@ -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==`

View File

@ -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.

20
eg/colorpicker/README.md Normal file
View File

@ -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.

95
eg/colorpicker/main.go Normal file
View File

@ -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()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

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

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

285
eg/forms/main.go Normal file
View File

@ -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
}

BIN
eg/forms/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -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)

11
eg/hello-world/README.md Normal file
View File

@ -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.

View File

@ -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,

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@ -1,7 +0,0 @@
package layout
import "fmt"
func main() {
fmt.Println("Hello world")
}

View File

@ -1,12 +0,0 @@
package main
import (
"fmt"
"git.kirsle.net/go/ui/eg/layout"
)
func main() {
fmt.Println("Hello world")
layout.main()
}

View File

@ -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.

BIN
eg/menus/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

11
eg/tabframe/README.md Normal file
View File

@ -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.

267
eg/tabframe/main.go Normal file
View File

@ -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,
})
}

BIN
eg/tabframe/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

11
eg/themes/Makefile Normal file
View File

@ -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

13
eg/themes/README.md Normal file
View File

@ -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.

107
eg/themes/main.go Normal file
View File

@ -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,
})
}

169
eg/themes/main_wasm.go Normal file
View File

@ -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,
})
}

BIN
eg/themes/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

10
eg/tooltip/README.md Normal file
View File

@ -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.

View File

@ -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.

BIN
eg/tooltip/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

11
eg/windows/README.md Normal file
View File

@ -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.

BIN
eg/windows/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -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),

View File

@ -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

View File

@ -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)
}

View File

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

31
glyphs.go Normal file
View File

@ -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
View File

@ -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
View File

@ -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
View File

@ -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
}
}

View File

@ -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 {

309
listbox.go Normal file
View File

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

503
magicform/magicform.go Normal file
View File

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

View File

@ -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.

229
pager.go Normal file
View File

@ -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)
}

254
scrollbar.go Normal file
View File

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

228
selectbox.go Normal file
View File

@ -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
})
}

95
style/button.go Normal file
View File

@ -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
}

13
style/style.go Normal file
View File

@ -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"
)

View File

@ -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)
}

300
tabframe.go Normal file
View File

@ -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,
})
}

6
theme.go Normal file
View File

@ -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

View File

@ -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,
},
}

View File

@ -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 {

View File

@ -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()
}

View File

@ -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() {}

View File

@ -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()
}
}

View File

@ -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