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:
Noah 2020-03-09 17:13:33 -07:00
parent 0846fe22fc
commit f9b305679a
20 changed files with 587 additions and 47 deletions

View File

@ -105,6 +105,9 @@ most complex.
* Pack() lets you add child widgets to the Frame, aligned against one side * 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 or another, and ability to expand widgets to take up remaining space in
their part of the Frame. 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. * [x] **Label**: Textual labels for your UI.
* Supports TrueType fonts, color, stroke, drop shadow, font size, etc. * Supports TrueType fonts, color, stroke, drop shadow, font size, etc.
* Variable binding support: TextVariable or IntVariable can point to a * 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, Background: theme.ButtonBackgroundColor,
}) })
w.Handle(MouseOver, func(p render.Point) { w.Handle(MouseOver, func(e EventData) {
w.hovering = true w.hovering = true
w.SetBackground(theme.ButtonHoverColor) w.SetBackground(theme.ButtonHoverColor)
}) })
w.Handle(MouseOut, func(p render.Point) { w.Handle(MouseOut, func(e EventData) {
w.hovering = false w.hovering = false
w.SetBackground(theme.ButtonBackgroundColor) w.SetBackground(theme.ButtonBackgroundColor)
}) })
w.Handle(MouseDown, func(p render.Point) { w.Handle(MouseDown, func(e EventData) {
w.clicked = true w.clicked = true
w.SetBorderStyle(BorderSunken) w.SetBorderStyle(BorderSunken)
}) })
w.Handle(MouseUp, func(p render.Point) { w.Handle(MouseUp, func(e EventData) {
w.clicked = false w.clicked = false
w.SetBorderStyle(BorderRaised) w.SetBorderStyle(BorderRaised)
}) })
@ -74,6 +74,8 @@ func (w *Button) Compute(e render.Engine) {
H: size.H + w.BoxThickness(2), H: size.H + w.BoxThickness(2),
}) })
} }
w.BaseWidget.Compute(e)
} }
// SetText conveniently sets the button text, for Label children only. // 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. // Draw the text label inside.
w.child.Present(e, moveTo) w.child.Present(e, moveTo)
w.BaseWidget.Present(e, P)
} }

View File

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

View File

@ -1,7 +1,5 @@
package ui package ui
import "git.kirsle.net/go/render"
// Checkbox combines a CheckButton with a widget like a Label. // Checkbox combines a CheckButton with a widget like a Label.
type Checkbox struct { type Checkbox struct {
Frame 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. // Forward clicks on the child widget to the CheckButton.
for _, e := range []Event{MouseOver, MouseOut, MouseUp, MouseDown} { for _, e := range []Event{MouseOver, MouseOut, MouseUp, MouseDown} {
func(e Event) { func(e Event) {
w.child.Handle(e, func(p render.Point) { w.child.Handle(e, func(ed EventData) {
w.button.Event(e, p) w.button.Event(e, ed)
}) })
}(e) }(e)
} }

View File

@ -134,7 +134,7 @@ func CreateButtons(window *ui.MainWindow, parent *ui.Frame) {
})) }))
// When clicked, change the window title to ID this button. // 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) window.SetTitle(parent.Name + ": " + setting.Label)
}) })

View File

@ -40,16 +40,12 @@ func main() {
Padding: 4, Padding: 4,
}, },
})) }))
button.Handle(ui.Click, func(p render.Point) { button.Handle(ui.Click, func(ed ui.EventData) {
fmt.Println("I've been clicked!") fmt.Println("I've been clicked!")
}) })
mw.Pack(button, ui.Pack{ mw.Pack(button, ui.Pack{
Side: ui.N, 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() mw.MainLoop()
} }

View File

@ -50,7 +50,7 @@ func main() {
btn := ui.NewButton(fmt.Sprintf("Button-%d", i), ui.NewLabel(ui.Label{ btn := ui.NewButton(fmt.Sprintf("Button-%d", i), ui.NewLabel(ui.Label{
Text: fmt.Sprintf("Button #%d", i), 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) 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 package ui
import ( import (
"errors"
"fmt" "fmt"
"git.kirsle.net/go/render" "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. // Children returns all of the child widgets.
func (w *Frame) Children() []Widget { func (w *Frame) Children() []Widget {
return w.widgets return w.widgets
@ -52,6 +71,9 @@ func (w *Frame) Children() []Widget {
func (w *Frame) Compute(e render.Engine) { func (w *Frame) Compute(e render.Engine) {
w.computePacked(e) w.computePacked(e)
w.computePlaced(e) w.computePlaced(e)
// Call the BaseWidget Compute in case we have subscribers.
w.BaseWidget.Compute(e)
} }
// Present the Frame. // 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.X+p.X+w.BoxThickness(1),
P.Y+p.Y+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) 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, widget: child,
pack: C, pack: C,
}) })
w.widgets = append(w.widgets, child) w.Add(child)
} }
// computePacked processes all the Pack layout widgets in the Frame. // computePacked processes all the Pack layout widgets in the Frame.

View File

@ -43,7 +43,7 @@ func (w *Frame) Place(child Widget, config Place) {
widget: child, widget: child,
place: config, place: config,
}) })
w.widgets = append(w.widgets, child) w.Add(child)
// Adopt the child widget so it can access the Frame. // Adopt the child widget so it can access the Frame.
child.SetParent(w) child.SetParent(w)
@ -61,6 +61,7 @@ func (w *Frame) computePlaced(e render.Engine) {
switch row.place.Strategy() { switch row.place.Strategy() {
case "Point": case "Point":
row.widget.MoveTo(row.place.Point) row.widget.MoveTo(row.place.Point)
row.widget.Compute(e)
case "Side": case "Side":
var moveTo render.Point 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) moveTo.Y = frameSize.H - (w.Size().H / 2) - (row.widget.Size().H / 2)
} }
row.widget.MoveTo(moveTo) row.widget.MoveTo(moveTo)
row.widget.Compute(e)
} }
// If this widget itself has placed widgets, call its function too. // If this widget itself has placed widgets, call its function too.

View File

@ -119,6 +119,9 @@ func (w *Image) GetRGBA() *image.RGBA {
// Compute the widget. // Compute the widget.
func (w *Image) Compute(e render.Engine) { func (w *Image) Compute(e render.Engine) {
w.Resize(w.texture.Size()) w.Resize(w.texture.Size())
// Call the BaseWidget Compute in case we have subscribers.
w.BaseWidget.Compute(e)
} }
// Present the widget. // Present the widget.
@ -131,4 +134,7 @@ func (w *Image) Present(e render.Engine, p render.Point) {
H: size.H, H: size.H,
} }
e.Copy(w.texture, size, dst) 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), H: maxRect.H + (padY * 2),
}) })
} }
// Call the BaseWidget Compute in case we have subscribers.
w.BaseWidget.Compute(e)
} }
// Present the label widget. // 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), Y: P.Y + border + padY + (i * w.lineHeight),
}) })
} }
// Call the BaseWidget Present in case we have subscribers.
w.BaseWidget.Present(e, P)
} }

View File

@ -69,7 +69,6 @@ func NewMainWindow(title string, dimensions ...int) (*MainWindow, error) {
// Add a default frame to the window. // Add a default frame to the window.
mw.frame = NewFrame("MainWindow Body") mw.frame = NewFrame("MainWindow Body")
mw.frame.SetBackground(render.RGBA(0, 153, 255, 100)) mw.frame.SetBackground(render.RGBA(0, 153, 255, 100))
mw.Add(mw.frame)
// Compute initial window size. // Compute initial window size.
mw.resized() mw.resized()

View File

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

View File

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

View File

@ -32,8 +32,8 @@ type Widget interface {
ResizeAuto(render.Rect) ResizeAuto(render.Rect)
Rect() render.Rect // Return the full absolute rect combining the Size() and Point() Rect() render.Rect // Return the full absolute rect combining the Size() and Point()
Handle(Event, func(render.Point)) Handle(Event, func(EventData))
Event(Event, render.Point) // called internally to trigger an event Event(Event, EventData) // called internally to trigger an event
// Thickness of the padding + border + outline. // Thickness of the padding + border + outline.
BoxThickness(multiplier int) int BoxThickness(multiplier int) int
@ -117,7 +117,7 @@ type BaseWidget struct {
borderSize int borderSize int
outlineColor render.Color outlineColor render.Color
outlineSize int outlineSize int
handlers map[Event][]func(render.Point) handlers map[Event][]func(EventData)
hasParent bool hasParent bool
parent Widget parent Widget
} }
@ -471,23 +471,40 @@ func (w *BaseWidget) SetOutlineSize(v int) {
w.outlineSize = v 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. // 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 { if handlers, ok := w.handlers[event]; ok {
for _, fn := range handlers { for _, fn := range handlers {
fn(p) fn(e)
} }
} }
} }
// Handle an event in the widget. // 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 { if w.handlers == nil {
w.handlers = map[Event][]func(render.Point){} w.handlers = map[Event][]func(EventData){}
} }
if _, ok := w.handlers[event]; !ok { 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) w.handlers[event] = append(w.handlers[event], fn)

View File

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