Compare commits
3 Commits
4ba563d48d
...
fb9127f0d5
Author | SHA1 | Date | |
---|---|---|---|
fb9127f0d5 | |||
f9b305679a | |||
0846fe22fc |
|
@ -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
|
||||
|
|
12
button.go
12
button.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
10
docs.go
Normal 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
29
eg/frame-place/README.md
Normal 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
144
eg/frame-place/main.go
Normal 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)
|
||||
}
|
||||
}
|
BIN
eg/frame-place/screenshot.png
Normal file
BIN
eg/frame-place/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
148
eg/tooltip/main.go
Normal 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
13
enums.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package ui
|
||||
|
||||
// Edge name
|
||||
type Edge int
|
||||
|
||||
// Edge values.
|
||||
const (
|
||||
Top Edge = iota
|
||||
Left
|
||||
Right
|
||||
Bottom
|
||||
FollowCursor
|
||||
)
|
39
frame.go
39
frame.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
99
frame_place.go
Normal 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
5
go.mod
Normal 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
7
go.sum
Normal 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=
|
55
image.go
55
image.go
|
@ -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)
|
||||
}
|
||||
|
|
6
label.go
6
label.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
8
menu.go
8
menu.go
|
@ -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()
|
||||
})
|
||||
|
||||
|
|
|
@ -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
294
tooltip.go
Normal 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
30
tooltip_test.go
Normal 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
4
version.go
Normal file
|
@ -0,0 +1,4 @@
|
|||
package ui
|
||||
|
||||
// Version of the UI toolkit.
|
||||
const Version = "0.1.0"
|
33
widget.go
33
widget.go
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue
Block a user