Compare commits

...

3 Commits

Author SHA1 Message Date
fb9127f0d5 Add version number and documentation 2020-03-09 17:29:17 -07:00
f9b305679a Tooltip Widget and Event Refactor
* Tooltip can be added to any target widget (e.g. Button) and pop up on
  mouse over.
* Refactor the event system. Instead of passing a render.Point to all
  event handlers, pass an EventData struct which can hold the Point or
  the render.Engine.
* Add event types Computed and Present, so a widget can set a handler on
  whenever its Computed or Present method is called.
2020-03-09 17:13:33 -07:00
0846fe22fc Place Strategy for Frame Widget 2020-03-08 22:07:46 -07:00
27 changed files with 999 additions and 52 deletions

View File

@ -105,6 +105,9 @@ most complex.
* 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.
* [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

View File

@ -35,20 +35,20 @@ func NewButton(name string, child Widget) *Button {
Background: theme.ButtonBackgroundColor,
})
w.Handle(MouseOver, func(p render.Point) {
w.Handle(MouseOver, func(e EventData) {
w.hovering = true
w.SetBackground(theme.ButtonHoverColor)
})
w.Handle(MouseOut, func(p render.Point) {
w.Handle(MouseOut, func(e EventData) {
w.hovering = false
w.SetBackground(theme.ButtonBackgroundColor)
})
w.Handle(MouseDown, func(p render.Point) {
w.Handle(MouseDown, func(e EventData) {
w.clicked = true
w.SetBorderStyle(BorderSunken)
})
w.Handle(MouseUp, func(p render.Point) {
w.Handle(MouseUp, func(e EventData) {
w.clicked = false
w.SetBorderStyle(BorderRaised)
})
@ -74,6 +74,8 @@ func (w *Button) Compute(e render.Engine) {
H: size.H + w.BoxThickness(2),
})
}
w.BaseWidget.Compute(e)
}
// SetText conveniently sets the button text, for Label children only.
@ -118,4 +120,6 @@ func (w *Button) Present(e render.Engine, P render.Point) {
// Draw the text label inside.
w.child.Present(e, moveTo)
w.BaseWidget.Present(e, P)
}

View File

@ -78,24 +78,24 @@ func (w *CheckButton) setup() {
Background: theme.ButtonBackgroundColor,
})
w.Handle(MouseOver, func(p render.Point) {
w.Handle(MouseOver, func(ed EventData) {
w.hovering = true
w.SetBackground(theme.ButtonHoverColor)
})
w.Handle(MouseOut, func(p render.Point) {
w.Handle(MouseOut, func(ed EventData) {
w.hovering = false
w.SetBackground(theme.ButtonBackgroundColor)
})
w.Handle(MouseDown, func(p render.Point) {
w.Handle(MouseDown, func(ed EventData) {
w.clicked = true
w.SetBorderStyle(BorderSunken)
})
w.Handle(MouseUp, func(p render.Point) {
w.Handle(MouseUp, func(ed EventData) {
w.clicked = false
})
w.Handle(Click, func(p render.Point) {
w.Handle(Click, func(ed EventData) {
var sunken bool
if w.BoolVar != nil {
if *w.BoolVar {

View File

@ -1,7 +1,5 @@
package ui
import "git.kirsle.net/go/render"
// Checkbox combines a CheckButton with a widget like a Label.
type Checkbox struct {
Frame
@ -37,8 +35,8 @@ func makeCheckbox(name string, boolVar *bool, stringVar *string, value string, c
// Forward clicks on the child widget to the CheckButton.
for _, e := range []Event{MouseOver, MouseOut, MouseUp, MouseDown} {
func(e Event) {
w.child.Handle(e, func(p render.Point) {
w.button.Event(e, p)
w.child.Handle(e, func(ed EventData) {
w.button.Event(e, ed)
})
}(e)
}

10
docs.go Normal file
View File

@ -0,0 +1,10 @@
/*
Package ui provides a user interface toolkit for Go.
The UI toolkit targets SDL2 applications on desktop (Linux, Mac and Windows)
or an HTML Canvas render engine for web browsers.
It provides various widgets such as Frame, Label, Button, Checkbox, Radiobox
and Tooltip and an event supervisor to monitor the state of the widgets.
*/
package ui

29
eg/frame-place/README.md Normal file
View File

@ -0,0 +1,29 @@
# Frame Placement Example
![Screenshot](screenshot.png)
## About
This demonstrates using the Place() method of the MainWindow and Frame to
position widgets around the window. The MainWindow itself and two child frames
(red and blue) are given the same set of buttons, placed relative to their own
parent widget.
The options for frame placing are:
* **Point:** Absolute X,Y coordinate relative to parent
* **Side:** binding the widget relative to a side of its parent.
* Top, Bottom, Left and Right to anchor to a side. In Bottom and Right, the
child widget's size is taken into account, so the right edge of the widget
would be `Right` pixels from the parent's right edge.
* Center and Middle options allow to anchor it to the center horizontally or
middle vertically.
Click any button and the title bar will update to show the name of the
button clicked and which parent it belonged to.
## Run it
```
go run main.go
```

144
eg/frame-place/main.go Normal file
View File

@ -0,0 +1,144 @@
// Example script for using the Place strategy of ui.Frame.
package main
import (
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
)
func main() {
mw, err := ui.NewMainWindow("Frame Placement Demo | Click a Button", 800, 600)
if err != nil {
panic(err)
}
mw.SetBackground(render.White)
// Create a sub-frame with its own buttons packed within.
frame := ui.NewFrame("Blue Frame")
frame.Configure(ui.Config{
Width: 300,
Height: 150,
Background: render.DarkBlue,
BorderSize: 1,
BorderStyle: ui.BorderSunken,
})
mw.Place(frame, ui.Place{
Point: render.NewPoint(80, 80),
})
// Create another frame that attaches itself to the bottom right
// of the window.
frame2 := ui.NewFrame("Red Frame")
frame2.Configure(ui.Config{
Width: 300,
Height: 150,
Background: render.DarkRed,
})
mw.Place(frame2, ui.Place{
Right: 80,
Bottom: 80,
})
// Draw rings of buttons around various widgets. The buttons say things
// like "Top Left", "Top Center", "Left Middle", "Center" etc. encompassing
// all 9 side placement options.
CreateButtons(mw, frame)
CreateButtons(mw, frame2)
CreateButtons(mw, mw.Frame())
mw.MainLoop()
}
// CreateButtons creates a set of Placed buttons around all the edges and
// center of the parent frame.
func CreateButtons(window *ui.MainWindow, parent *ui.Frame) {
// Draw buttons around the edges of the window.
buttons := []struct {
Label string
Place ui.Place
}{
{
Label: "Top Left",
Place: ui.Place{
Point: render.NewPoint(12, 12),
},
},
{
Label: "Top Middle",
Place: ui.Place{
Top: 12,
Center: true,
},
},
{
Label: "Top Right",
Place: ui.Place{
Top: 12,
Right: 12,
},
},
{
Label: "Left Middle",
Place: ui.Place{
Left: 12,
Middle: true,
},
},
{
Label: "Center",
Place: ui.Place{
Center: true,
Middle: true,
},
},
{
Label: "Right Middle",
Place: ui.Place{
Right: 12,
Middle: true,
},
},
{
Label: "Bottom Left",
Place: ui.Place{
Left: 12,
Bottom: 12,
},
},
{
Label: "Bottom Center",
Place: ui.Place{
Bottom: 12,
Center: true,
},
},
{
Label: "Bottom Right",
Place: ui.Place{
Bottom: 12,
Right: 12,
},
},
}
for _, setting := range buttons {
setting := setting
button := ui.NewButton(setting.Label, ui.NewLabel(ui.Label{
Text: setting.Label,
Font: render.Text{
FontFilename: "../DejaVuSans.ttf",
Size: 12,
Color: render.Black,
},
}))
// When clicked, change the window title to ID this button.
button.Handle(ui.Click, func(ed ui.EventData) {
window.SetTitle(parent.Name + ": " + setting.Label)
})
parent.Place(button, setting.Place)
window.Add(button)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -40,16 +40,12 @@ func main() {
Padding: 4,
},
}))
button.Handle(ui.Click, func(p render.Point) {
button.Handle(ui.Click, func(ed ui.EventData) {
fmt.Println("I've been clicked!")
})
mw.Pack(button, ui.Pack{
Side: ui.N,
})
// Add the button to the MainWindow's Supervisor so it can be
// clicked on and interacted with.
mw.Add(button)
mw.MainLoop()
}

View File

@ -50,7 +50,7 @@ func main() {
btn := ui.NewButton(fmt.Sprintf("Button-%d", i), ui.NewLabel(ui.Label{
Text: fmt.Sprintf("Button #%d", i),
}))
btn.Handle(ui.Click, func(p render.Point) {
btn.Handle(ui.Click, func(ed ui.EventData) {
fmt.Printf("Button %d was clicked\n", i)
})

148
eg/tooltip/main.go Normal file
View File

@ -0,0 +1,148 @@
package main
import (
"git.kirsle.net/go/render"
"git.kirsle.net/go/render/sdl"
"git.kirsle.net/go/ui"
)
func init() {
sdl.DefaultFontFilename = "../DejaVuSans.ttf"
}
func main() {
mw, err := ui.NewMainWindow("Tooltip Demo", 800, 600)
if err != nil {
panic(err)
}
mw.SetBackground(render.White)
CreateButtons(mw, mw.Frame())
btn := ui.NewButton("Test", ui.NewLabel(ui.Label{
Text: "Click me",
Font: render.Text{
Size: 32,
},
}))
mw.Place(btn, ui.Place{
Center: true,
Middle: true,
})
ui.NewTooltip(btn, ui.Tooltip{
Text: "Hello world\nGoodbye mars!\nBlah blah blah...\nLOL",
Edge: ui.Right,
})
mw.MainLoop()
}
// CreateButtons creates a set of Placed buttons around all the edges and
// center of the parent frame.
func CreateButtons(window *ui.MainWindow, parent *ui.Frame) {
// Draw buttons around the edges of the window.
buttons := []struct {
Label string
Edge ui.Edge
Place ui.Place
}{
{
Label: "Top Left",
Edge: ui.Right,
Place: ui.Place{
Point: render.NewPoint(12, 12),
},
},
{
Label: "Top Middle",
Edge: ui.Bottom,
Place: ui.Place{
Top: 12,
Center: true,
},
},
{
Label: "Top Right",
Edge: ui.Left,
Place: ui.Place{
Top: 12,
Right: 12,
},
},
{
Label: "Left Middle",
Edge: ui.Right,
Place: ui.Place{
Left: 12,
Middle: true,
},
},
{
Label: "Center",
Edge: ui.Bottom,
Place: ui.Place{
Center: true,
Middle: true,
},
},
{
Label: "Right Middle",
Edge: ui.Left,
Place: ui.Place{
Right: 12,
Middle: true,
},
},
{
Label: "Bottom Left",
Edge: ui.Right,
Place: ui.Place{
Left: 12,
Bottom: 12,
},
},
{
Label: "Bottom Center",
Edge: ui.Top,
Place: ui.Place{
Bottom: 12,
Center: true,
},
},
{
Label: "Bottom Right",
Edge: ui.Left,
Place: ui.Place{
Bottom: 12,
Right: 12,
},
},
}
for _, setting := range buttons {
setting := setting
button := ui.NewButton(setting.Label, ui.NewLabel(ui.Label{
Text: setting.Label,
Font: render.Text{
FontFilename: "../DejaVuSans.ttf",
Size: 12,
Color: render.Black,
},
}))
// When clicked, change the window title to ID this button.
button.Handle(ui.Click, func(ed ui.EventData) {
window.SetTitle(parent.Name + ": " + setting.Label)
})
// Tooltip for it.
ui.NewTooltip(button, ui.Tooltip{
Text: setting.Label + " Tooltip",
Edge: setting.Edge,
})
parent.Place(button, setting.Place)
window.Add(button)
}
}

13
enums.go Normal file
View File

@ -0,0 +1,13 @@
package ui
// Edge name
type Edge int
// Edge values.
const (
Top Edge = iota
Left
Right
Bottom
FollowCursor
)

View File

@ -1,6 +1,7 @@
package ui
import (
"errors"
"fmt"
"git.kirsle.net/go/render"
@ -10,7 +11,10 @@ import (
type Frame struct {
Name string
BaseWidget
packs map[Side][]packedWidget
// Widget placement settings.
packs map[Side][]packedWidget // Packed widgets
placed []placedWidget // Placed widgets
widgets []Widget
}
@ -40,6 +44,24 @@ func (w *Frame) Setup() {
}
}
// Add a child widget to the frame. When the frame Presents itself, it also
// presents child widgets. This method is safe to call multiple times: it ensures
// the widget is not already a child of the Frame before adding it.
func (w *Frame) Add(child Widget) error {
if child == w {
return errors.New("can't add self to frame")
}
// Ensure child is new to the frame.
for _, widget := range w.widgets {
if widget == child {
return errors.New("widget already added to frame")
}
}
w.widgets = append(w.widgets, child)
return nil
}
// Children returns all of the child widgets.
func (w *Frame) Children() []Widget {
return w.widgets
@ -48,6 +70,10 @@ func (w *Frame) Children() []Widget {
// Compute the size of the Frame.
func (w *Frame) Compute(e render.Engine) {
w.computePacked(e)
w.computePlaced(e)
// Call the BaseWidget Compute in case we have subscribers.
w.BaseWidget.Compute(e)
}
// Present the Frame.
@ -79,14 +105,9 @@ func (w *Frame) Present(e render.Engine, P render.Point) {
P.X+p.X+w.BoxThickness(1),
P.Y+p.Y+w.BoxThickness(1),
)
// if child.ID() == "Canvas" {
// log.Debug("Frame X=%d Child X=%d Box=%d Point=%s", P.X, p.X, w.BoxThickness(1), p)
// log.Debug("Frame Y=%d Child Y=%d Box=%d MoveTo=%s", P.Y, p.Y, w.BoxThickness(1), moveTo)
// }
// child.MoveTo(moveTo) // TODO: if uncommented the child will creep down the parent each tick
// if child.ID() == "Canvas" {
// log.Debug("New Point: %s", child.Point())
// }
child.Present(e, moveTo)
}
// Call the BaseWidget Present in case we have subscribers.
w.BaseWidget.Present(e, P)
}

View File

@ -53,7 +53,7 @@ func (w *Frame) Pack(child Widget, config ...Pack) {
widget: child,
pack: C,
})
w.widgets = append(w.widgets, child)
w.Add(child)
}
// computePacked processes all the Pack layout widgets in the Frame.

99
frame_place.go Normal file
View File

@ -0,0 +1,99 @@
package ui
import (
"git.kirsle.net/go/render"
)
// Place provides configuration fields for Frame.Place().
type Place struct {
// X and Y coordinates for explicit location of widget within its parent.
// This placement option trumps all others.
Point render.Point
// Place relative to an edge of the window. The widget will stick to the
// edge of the window even as it resizes. Options are ignored if Point
// is set.
Top int
Left int
Right int
Bottom int
Center bool
Middle bool
}
// Strategy returns the placement strategy for a Place config struct.
// Returns 'Point' if a render.Point is used (even if zero, zero)
// Returns 'Side' if the side values are set.
func (p Place) Strategy() string {
if p.Top != 0 || p.Left != 0 || p.Right != 0 || p.Bottom != 0 || p.Center || p.Middle {
return "Side"
}
return "Point"
}
// placedWidget holds the data for a widget placed in a frame.
type placedWidget struct {
widget Widget
place Place
}
// Place a widget into the frame.
func (w *Frame) Place(child Widget, config Place) {
w.placed = append(w.placed, placedWidget{
widget: child,
place: config,
})
w.Add(child)
// Adopt the child widget so it can access the Frame.
child.SetParent(w)
}
// computePlaced processes all the Place layout widgets in the Frame,
// determining their X,Y location and whether they need to change.
func (w *Frame) computePlaced(e render.Engine) {
var (
frameSize = w.BoxSize()
)
for _, row := range w.placed {
// X,Y placement takes priority.
switch row.place.Strategy() {
case "Point":
row.widget.MoveTo(row.place.Point)
row.widget.Compute(e)
case "Side":
var moveTo render.Point
// Compute the initial X,Y based on Top, Left, Right, Bottom.
if row.place.Left > 0 {
moveTo.X = row.place.Left
}
if row.place.Top > 0 {
moveTo.Y = row.place.Top
}
if row.place.Right > 0 {
moveTo.X = frameSize.W - row.widget.Size().W - row.place.Right
}
if row.place.Bottom > 0 {
moveTo.Y = frameSize.H - row.widget.Size().H - row.place.Bottom
}
// Center and Middle aligned values override Left/Right, Top/Bottom
// settings respectively.
if row.place.Center {
moveTo.X = frameSize.W - (w.Size().W / 2) - (row.widget.Size().W / 2)
}
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)
}
// If this widget itself has placed widgets, call its function too.
if frame, ok := row.widget.(*Frame); ok {
frame.computePlaced(e)
}
}
}

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module git.kirsle.net/go/ui
go 1.13
require git.kirsle.net/go/render v0.0.0-20200102014411-4d008b5c468d

7
go.sum Normal file
View File

@ -0,0 +1,7 @@
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=
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=
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -2,6 +2,9 @@ package ui
import (
"fmt"
"image"
"image/jpeg"
"os"
"path/filepath"
"strings"
@ -13,8 +16,9 @@ type ImageType string
// Supported image formats.
const (
BMP ImageType = "bmp"
PNG = "png"
BMP ImageType = "bmp"
PNG = "png"
JPEG = "jpg"
)
// Image is a widget that is backed by an image file.
@ -23,6 +27,7 @@ type Image struct {
// Configurable fields for the constructor.
Type ImageType
Image image.Image
texture render.Texturer
}
@ -48,6 +53,29 @@ func ImageFromTexture(tex render.Texturer) *Image {
}
}
// ImageFromFile creates an Image by opening a file from disk.
func ImageFromFile(e render.Engine, filename string) (*Image, error) {
fh, err := os.Open(filename)
if err != nil {
return nil, err
}
img, err := jpeg.Decode(fh)
if err != nil {
return nil, err
}
tex, err := e.StoreTexture(filename, img)
if err != nil {
return nil, err
}
return &Image{
Image: img,
texture: tex,
}, nil
}
// OpenImage initializes an Image with a given file name.
//
// The file extension is important and should be a supported ImageType.
@ -58,6 +86,10 @@ func OpenImage(e render.Engine, filename string) (*Image, error) {
w.Type = BMP
case ".png":
w.Type = PNG
case ".jpg":
w.Type = JPEG
case ".jpeg":
w.Type = JPEG
default:
return nil, fmt.Errorf("OpenImage: %s: not a supported image type", filename)
}
@ -71,9 +103,25 @@ func OpenImage(e render.Engine, filename string) (*Image, error) {
return w, nil
}
// GetRGBA returns an image.RGBA from the image data.
func (w *Image) GetRGBA() *image.RGBA {
var bounds = w.Image.Bounds()
var rgba = image.NewRGBA(bounds)
for x := bounds.Min.X; x < bounds.Max.X; x++ {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
color := w.Image.At(x, y)
rgba.Set(x, y, color)
}
}
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)
}
// Present the widget.
@ -86,4 +134,7 @@ func (w *Image) Present(e render.Engine, p render.Point) {
H: size.H,
}
e.Copy(w.texture, size, dst)
// Call the BaseWidget Present in case we have subscribers.
w.BaseWidget.Present(e, p)
}

View File

@ -101,6 +101,9 @@ func (w *Label) Compute(e render.Engine) {
H: maxRect.H + (padY * 2),
})
}
// Call the BaseWidget Compute in case we have subscribers.
w.BaseWidget.Compute(e)
}
// Present the label widget.
@ -125,4 +128,7 @@ func (w *Label) Present(e render.Engine, P render.Point) {
Y: P.Y + border + padY + (i * w.lineHeight),
})
}
// Call the BaseWidget Present in case we have subscribers.
w.BaseWidget.Present(e, P)
}

View File

@ -16,6 +16,12 @@ var (
FPS = 60
)
// Default width and height for MainWindow.
var (
DefaultWidth = 640
DefaultHeight = 480
)
// MainWindow is the parent window of a UI application.
type MainWindow struct {
Engine render.Engine
@ -27,11 +33,26 @@ type MainWindow struct {
}
// NewMainWindow initializes the MainWindow. You should probably only have one
// of these per application.
func NewMainWindow(title string) (*MainWindow, error) {
// of these per application. Dimensions are the width and height of the window.
//
// Example: NewMainWindow("Title Bar") // default 640x480 window
// NewMainWindow("Title", 800, 600) // both required
func NewMainWindow(title string, dimensions ...int) (*MainWindow, error) {
var (
width = DefaultWidth
height = DefaultHeight
)
if len(dimensions) > 0 {
if len(dimensions) != 2 {
return nil, fmt.Errorf("provide width and height dimensions, like NewMainWindow(title, 800, 600)")
}
width, height = dimensions[0], dimensions[1]
}
mw := &MainWindow{
w: 800,
h: 600,
w: width,
h: height,
supervisor: NewSupervisor(),
loopCallbacks: []func(*event.State){},
}
@ -48,7 +69,6 @@ func NewMainWindow(title string) (*MainWindow, error) {
// Add a default frame to the window.
mw.frame = NewFrame("MainWindow Body")
mw.frame.SetBackground(render.RGBA(0, 153, 255, 100))
mw.Add(mw.frame)
// Compute initial window size.
mw.resized()
@ -56,6 +76,11 @@ func NewMainWindow(title string) (*MainWindow, error) {
return mw, nil
}
// SetTitle changes the title of the window.
func (mw *MainWindow) SetTitle(title string) {
mw.Engine.SetTitle(title)
}
// Add a child widget to the window.
func (mw *MainWindow) Add(w Widget) {
mw.supervisor.Add(w)
@ -67,6 +92,12 @@ func (mw *MainWindow) Pack(w Widget, pack Pack) {
mw.frame.Pack(w, pack)
}
// Place a child widget into the window's default frame.
func (mw *MainWindow) Place(w Widget, config Place) {
mw.Add(w)
mw.frame.Place(w, config)
}
// Frame returns the window's main frame, if needed.
func (mw *MainWindow) Frame() *Frame {
return mw.frame

View File

@ -36,11 +36,17 @@ func NewMenu(name string) *Menu {
// Compute the menu
func (w *Menu) Compute(e render.Engine) {
w.body.Compute(e)
// Call the BaseWidget Compute in case we have subscribers.
w.BaseWidget.Compute(e)
}
// Present the menu
func (w *Menu) Present(e render.Engine, p render.Point) {
w.body.Present(e, p)
// Call the BaseWidget Present in case we have subscribers.
w.BaseWidget.Present(e, p)
}
// AddItem quickly adds an item to a menu.
@ -90,7 +96,7 @@ func NewMenuItem(label string, command func()) *MenuItem {
Background: render.Blue,
})
w.Button.Handle(Click, func(p render.Point) {
w.Button.Handle(Click, func(ed EventData) {
w.Command()
})

View File

@ -24,8 +24,19 @@ const (
KeyUp
KeyPress
Drop
Compute // fired whenever the widget runs Compute
Present // fired whenever the widget runs Present
)
// EventData carries common data to event handlers.
type EventData struct {
// Point is usually the cursor position on click and mouse events.
Point render.Point
// Engine is the render engine on Compute and Present events.
Engine render.Engine
}
// Supervisor keeps track of widgets of interest to notify them about
// interaction events such as mouse hovers and clicks in their general
// vicinity.
@ -97,7 +108,9 @@ func (s *Supervisor) Loop(ev *event.State) error {
if !ev.Button1 && !ev.Button3 {
// The mouse has been released. TODO: make mouse button important?
for _, child := range hovering {
child.widget.Event(Drop, XY)
child.widget.Event(Drop, EventData{
Point: XY,
})
}
s.DragStop()
}
@ -117,19 +130,27 @@ func (s *Supervisor) Loop(ev *event.State) error {
// Cursor has intersected the widget.
if _, ok := s.hovering[id]; !ok {
w.Event(MouseOver, XY)
w.Event(MouseOver, EventData{
Point: XY,
})
s.hovering[id] = nil
}
_, isClicked := s.clicked[id]
if ev.Button1 {
if !isClicked {
w.Event(MouseDown, XY)
w.Event(MouseDown, EventData{
Point: XY,
})
s.clicked[id] = nil
}
} else if isClicked {
w.Event(MouseUp, XY)
w.Event(Click, XY)
w.Event(MouseUp, EventData{
Point: XY,
})
w.Event(Click, EventData{
Point: XY,
})
delete(s.clicked, id)
}
}
@ -141,12 +162,16 @@ func (s *Supervisor) Loop(ev *event.State) error {
// Cursor is not intersecting the widget.
if _, ok := s.hovering[id]; ok {
w.Event(MouseOut, XY)
w.Event(MouseOut, EventData{
Point: XY,
})
delete(s.hovering, id)
}
if _, ok := s.clicked[id]; ok {
w.Event(MouseUp, XY)
w.Event(MouseUp, EventData{
Point: XY,
})
delete(s.clicked, id)
}
}

294
tooltip.go Normal file
View File

@ -0,0 +1,294 @@
package ui
import (
"fmt"
"strings"
"git.kirsle.net/go/render"
)
func init() {
precomputeArrows()
}
// Tooltip attaches a mouse-over popup to another widget.
type Tooltip struct {
BaseWidget
// Configurable attributes.
Text string // Text to show in the tooltip.
TextVariable *string // String pointer instead of text.
Edge Edge // side to display tooltip on
target Widget
lineHeight int
font render.Text
}
// Constants for tooltips.
const (
tooltipArrowSize = 5
)
// NewTooltip creates a new tooltip attached to a widget.
func NewTooltip(target Widget, tt Tooltip) *Tooltip {
w := &Tooltip{
Text: tt.Text,
TextVariable: tt.TextVariable,
Edge: tt.Edge,
target: target,
}
// Default style.
w.Hide()
w.SetBackground(render.RGBA(0, 0, 0, 230))
w.font = render.Text{
Size: 10,
Color: render.White,
Padding: 4,
}
// Add event bindings to the target widget.
// - Show the tooltip on MouseOver
// - Hide it on MouseOut
// - Compute the tooltip when the parent widget Computes
// - Present the tooltip when the parent widget Presents
target.Handle(MouseOver, func(ed EventData) {
w.Show()
})
target.Handle(MouseOut, func(ed EventData) {
w.Hide()
})
target.Handle(Compute, func(ed EventData) {
w.Compute(ed.Engine)
})
target.Handle(Present, func(ed EventData) {
w.Present(ed.Engine, w.Point())
})
w.IDFunc(func() string {
return fmt.Sprintf(`Tooltip<"%s">`, w.Value())
})
return w
}
// Value returns the current text displayed in the tooltop, whether from the
// configured Text or the TextVariable pointer.
func (w *Tooltip) Value() string {
return w.text().Text
}
// text returns the raw render.Text holding the current value to be displayed
// in the tooltip, either from Text or TextVariable.
func (w *Tooltip) text() render.Text {
if w.TextVariable != nil {
w.font.Text = *w.TextVariable
} else {
w.font.Text = w.Text
}
return w.font
}
// Compute the size of the tooltip.
func (w *Tooltip) Compute(e render.Engine) {
// Compute the size based on the text.
w.computeText(e)
// Compute the position based on the Edge and the target widget.
var (
size = w.Size()
target = w.target
tSize = target.Size()
tPoint = AbsolutePosition(target)
moveTo render.Point
)
switch w.Edge {
case Top:
moveTo.Y = tPoint.Y - size.H - tooltipArrowSize
moveTo.X = tPoint.X + (tSize.W / 2) - (size.W / 2)
case Left:
moveTo.X = tPoint.X - size.W - tooltipArrowSize
moveTo.Y = tPoint.Y + (tSize.H / 2) - (size.H / 2)
case Right:
moveTo.X = tPoint.X + tSize.W + tooltipArrowSize
moveTo.Y = tPoint.Y + (tSize.H / 2) - (size.H / 2)
case Bottom:
moveTo.Y = tPoint.Y + tSize.H + tooltipArrowSize
moveTo.X = tPoint.X + (tSize.W / 2) - (size.W / 2)
}
w.MoveTo(moveTo)
}
// computeText handles the text compute, very similar to Label.Compute.
func (w *Tooltip) computeText(e render.Engine) {
text := w.text()
lines := strings.Split(text.Text, "\n")
// Max rect to encompass all lines of text.
var maxRect = render.Rect{}
for _, line := range lines {
if line == "" {
line = "<empty>"
}
text.Text = line // only this line at this time.
rect, err := e.ComputeTextRect(text)
if err != nil {
panic(fmt.Sprintf("%s: failed to compute text rect: %s", w, err)) // TODO return an error
}
if rect.W > maxRect.W {
maxRect.W = rect.W
}
maxRect.H += rect.H
w.lineHeight = int(rect.H)
}
var (
padX = w.font.Padding + w.font.PadX
padY = w.font.Padding + w.font.PadY
)
w.Resize(render.Rect{
W: maxRect.W + (padX * 2),
H: maxRect.H + (padY * 2),
})
}
// Present the tooltip.
func (w *Tooltip) Present(e render.Engine, P render.Point) {
if w.Hidden() {
return
}
// Draw the text.
w.presentText(e, P)
// Draw the arrow.
w.presentArrow(e, P)
}
// presentText draws the text similar to Label.
func (w *Tooltip) presentText(e render.Engine, P render.Point) {
var (
text = w.text()
padX = w.font.Padding + w.font.PadX
padY = w.font.Padding + w.font.PadY
)
w.DrawBox(e, P)
for i, line := range strings.Split(text.Text, "\n") {
text.Text = line
e.DrawText(text, render.Point{
X: P.X + padX,
Y: P.Y + padY + (i * w.lineHeight),
})
}
}
// presentArrow draws the arrow between the tooltip and its target widget.
func (w *Tooltip) presentArrow(e render.Engine, P render.Point) {
var (
// size = w.Size()
target = w.target
tSize = target.Size()
tPoint = AbsolutePosition(target)
drawAt render.Point
arrow [][]render.Point
)
switch w.Edge {
case Top:
arrow = arrowDown
drawAt = render.Point{
X: tPoint.X + (tSize.W / 2) - tooltipArrowSize,
Y: tPoint.Y - tooltipArrowSize,
}
case Bottom:
arrow = arrowUp
drawAt = render.Point{
X: tPoint.X + (tSize.W / 2) - tooltipArrowSize,
Y: tPoint.Y + tSize.H,
}
case Left:
arrow = arrowRight
drawAt = render.Point{
X: tPoint.X - tooltipArrowSize,
Y: tPoint.Y + (tSize.H / 2) - tooltipArrowSize,
}
case Right:
arrow = arrowLeft
drawAt = render.Point{
X: tPoint.X + tSize.W,
Y: tPoint.Y + (tSize.H / 2) - tooltipArrowSize,
}
}
drawArrow(e, w.Background(), drawAt, arrow)
}
// Draw an arrow at a given top/left coordinate.
func drawArrow(e render.Engine, color render.Color, p render.Point, arrow [][]render.Point) {
for _, row := range arrow {
if len(row) == 1 {
point := render.NewPoint(row[0].X, row[0].Y)
point.Add(p)
e.DrawPoint(color, point)
} else {
start := render.NewPoint(row[0].X, row[0].Y)
end := render.NewPoint(row[1].X, row[1].Y)
start.Add(p)
end.Add(p)
e.DrawLine(color, start, end)
}
}
}
// Arrows for the tooltip widget.
var (
arrowDown [][]render.Point
arrowUp [][]render.Point
arrowLeft [][]render.Point
arrowRight [][]render.Point
)
func precomputeArrows() {
arrowDown = [][]render.Point{
{render.NewPoint(0, 0), render.NewPoint(10, 0)},
{render.NewPoint(1, 1), render.NewPoint(9, 1)},
{render.NewPoint(2, 2), render.NewPoint(8, 2)},
{render.NewPoint(3, 3), render.NewPoint(7, 3)},
{render.NewPoint(4, 4), render.NewPoint(6, 4)},
{render.NewPoint(5, 5)},
}
arrowUp = [][]render.Point{
{render.NewPoint(5, 0)},
{render.NewPoint(4, 1), render.NewPoint(6, 1)},
{render.NewPoint(3, 2), render.NewPoint(7, 2)},
{render.NewPoint(2, 3), render.NewPoint(8, 3)},
{render.NewPoint(1, 4), render.NewPoint(9, 4)},
// {render.NewPoint(0, 5), render.NewPoint(10, 5)},
}
arrowLeft = [][]render.Point{
{render.NewPoint(0, 5)},
{render.NewPoint(1, 4), render.NewPoint(1, 6)},
{render.NewPoint(2, 3), render.NewPoint(2, 7)},
{render.NewPoint(3, 2), render.NewPoint(3, 8)},
{render.NewPoint(4, 1), render.NewPoint(4, 9)},
// {render.NewPoint(5, 0), render.NewPoint(5, 10)},
}
arrowRight = [][]render.Point{
{render.NewPoint(0, 0), render.NewPoint(0, 10)},
{render.NewPoint(1, 1), render.NewPoint(1, 9)},
{render.NewPoint(2, 2), render.NewPoint(2, 8)},
{render.NewPoint(3, 3), render.NewPoint(3, 7)},
{render.NewPoint(4, 4), render.NewPoint(4, 6)},
{render.NewPoint(5, 5)},
}
}

30
tooltip_test.go Normal file
View File

@ -0,0 +1,30 @@
package ui_test
import "git.kirsle.net/go/ui"
// Tooltip usage example.
func ExampleTooltip() {
mw, err := ui.NewMainWindow("Tooltip Example", 800, 600)
if err != nil {
panic(err)
}
// Add a widget that will have a tooltip attached, i.e. a button.
btn := ui.NewButton("My Button", ui.NewLabel(ui.Label{
Text: "Hello world!",
}))
mw.Place(btn, ui.Place{
Center: true,
Middle: true,
})
// 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{
Text: "This is a tooltip that pops up\non mouse hover!",
Edge: ui.Right,
})
mw.MainLoop()
}

4
version.go Normal file
View File

@ -0,0 +1,4 @@
package ui
// Version of the UI toolkit.
const Version = "0.1.0"

View File

@ -32,8 +32,8 @@ type Widget interface {
ResizeAuto(render.Rect)
Rect() render.Rect // Return the full absolute rect combining the Size() and Point()
Handle(Event, func(render.Point))
Event(Event, render.Point) // called internally to trigger an event
Handle(Event, func(EventData))
Event(Event, EventData) // called internally to trigger an event
// Thickness of the padding + border + outline.
BoxThickness(multiplier int) int
@ -117,7 +117,7 @@ type BaseWidget struct {
borderSize int
outlineColor render.Color
outlineSize int
handlers map[Event][]func(render.Point)
handlers map[Event][]func(EventData)
hasParent bool
parent Widget
}
@ -471,23 +471,40 @@ func (w *BaseWidget) SetOutlineSize(v int) {
w.outlineSize = v
}
// Compute calls the base widget's Compute function, which just triggers
// events on widgets that want to be notified when the widget computes.
func (w *BaseWidget) Compute(e render.Engine) {
w.Event(Compute, EventData{
Engine: e,
})
}
// Present calls the base widget's Present function, which just triggers
// events on widgets that want to be notified when the widget presents.
func (w *BaseWidget) Present(e render.Engine, p render.Point) {
w.Event(Present, EventData{
Point: p,
Engine: e,
})
}
// Event is called internally by Doodle to trigger an event.
func (w *BaseWidget) Event(event Event, p render.Point) {
func (w *BaseWidget) Event(event Event, e EventData) {
if handlers, ok := w.handlers[event]; ok {
for _, fn := range handlers {
fn(p)
fn(e)
}
}
}
// Handle an event in the widget.
func (w *BaseWidget) Handle(event Event, fn func(render.Point)) {
func (w *BaseWidget) Handle(event Event, fn func(EventData)) {
if w.handlers == nil {
w.handlers = map[Event][]func(render.Point){}
w.handlers = map[Event][]func(EventData){}
}
if _, ok := w.handlers[event]; !ok {
w.handlers[event] = []func(render.Point){}
w.handlers[event] = []func(EventData){}
}
w.handlers[event] = append(w.handlers[event], fn)

View File

@ -104,11 +104,17 @@ func (w *Window) ConfigureTitle(C Config) {
// Compute the window.
func (w *Window) Compute(e render.Engine) {
w.body.Compute(e)
// Call the BaseWidget Compute in case we have subscribers.
w.BaseWidget.Compute(e)
}
// Present the window.
func (w *Window) Present(e render.Engine, P render.Point) {
w.body.Present(e, P)
// Call the BaseWidget Present in case we have subscribers.
w.BaseWidget.Present(e, P)
}
// Pack a widget into the window's frame.