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.
This commit is contained in:
parent
0846fe22fc
commit
f9b305679a
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -134,7 +134,7 @@ func CreateButtons(window *ui.MainWindow, parent *ui.Frame) {
|
|||
}))
|
||||
|
||||
// When clicked, change the window title to ID this button.
|
||||
button.Handle(ui.Click, func(p render.Point) {
|
||||
button.Handle(ui.Click, func(ed ui.EventData) {
|
||||
window.SetTitle(parent.Name + ": " + setting.Label)
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
33
frame.go
33
frame.go
|
@ -1,6 +1,7 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
|
@ -43,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
|
||||
|
@ -52,6 +71,9 @@ func (w *Frame) Children() []Widget {
|
|||
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.
|
||||
|
@ -83,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.
|
||||
|
|
|
@ -43,7 +43,7 @@ func (w *Frame) Place(child Widget, config Place) {
|
|||
widget: child,
|
||||
place: config,
|
||||
})
|
||||
w.widgets = append(w.widgets, child)
|
||||
w.Add(child)
|
||||
|
||||
// Adopt the child widget so it can access the Frame.
|
||||
child.SetParent(w)
|
||||
|
@ -61,6 +61,7 @@ func (w *Frame) computePlaced(e render.Engine) {
|
|||
switch row.place.Strategy() {
|
||||
case "Point":
|
||||
row.widget.MoveTo(row.place.Point)
|
||||
row.widget.Compute(e)
|
||||
case "Side":
|
||||
var moveTo render.Point
|
||||
|
||||
|
@ -87,6 +88,7 @@ func (w *Frame) computePlaced(e render.Engine) {
|
|||
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.
|
||||
|
|
6
image.go
6
image.go
|
@ -119,6 +119,9 @@ func (w *Image) GetRGBA() *image.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.
|
||||
|
@ -131,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)
|
||||
}
|
||||
|
|
|
@ -69,7 +69,6 @@ func NewMainWindow(title string, dimensions ...int) (*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()
|
||||
|
|
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)},
|
||||
}
|
||||
}
|
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