Add ui.Supervisor for Widget Event Handling

The Buttons can now be managed by a ui.Supervisor and be notified when
the mouse enters or leaves their bounding box and handle click events.

Current event handlers supported:
* MouseOver
* MouseOut
* MouseDown
* MouseUp
* Click

Each of those events are only fired when the state of the event has
changed, i.e. the first time the mouse enters the widget MouseOver is
called and then when the mouse leaves later, MouseOut is called.

A completed click event (mouse was released while pressed and hovering
the button) triggers both MouseOut and Click, so the button can pop
itself out and also run the click handler.
This commit is contained in:
Noah 2018-07-25 20:25:02 -07:00
parent 41e1838549
commit 602273aa16
6 changed files with 208 additions and 38 deletions

View File

@ -8,6 +8,7 @@ import (
// MainScene implements the main menu of Doodle.
type MainScene struct {
Supervisor *ui.Supervisor
}
// Name of the scene.
@ -17,11 +18,43 @@ func (s *MainScene) Name() string {
// Setup the scene.
func (s *MainScene) Setup(d *Doodle) error {
s.Supervisor = ui.NewSupervisor()
button1 := ui.NewButton(*ui.NewLabel(render.Text{
Text: "New Map",
Size: 14,
Color: render.Black,
}))
button1.Compute(d.Engine)
button1.MoveTo(render.Point{
X: (d.width / 2) - (button1.Size().W / 2),
Y: 200,
})
button1.Handle("Click", func(p render.Point) {
d.NewMap()
})
button2 := ui.NewButton(*ui.NewLabel(render.Text{
Text: "New Map",
Size: 14,
Color: render.Black,
}))
button2.SetText("Load Map")
button2.Compute(d.Engine)
button2.MoveTo(render.Point{
X: (d.width / 2) - (button2.Size().W / 2),
Y: 260,
})
s.Supervisor.Add(button1)
s.Supervisor.Add(button2)
return nil
}
// Loop the editor scene.
func (s *MainScene) Loop(d *Doodle, ev *events.State) error {
s.Supervisor.Loop(ev)
return nil
}
@ -44,26 +77,7 @@ func (s *MainScene) Draw(d *Doodle) error {
})
label.Present(d.Engine)
button := ui.NewButton(*ui.NewLabel(render.Text{
Text: "New Map",
Size: 14,
Color: render.Black,
}))
button.Compute(d.Engine)
button.MoveTo(render.Point{
X: (d.width / 2) - (button.Size().W / 2),
Y: 200,
})
button.Present(d.Engine)
button.SetText("Load Map")
button.Compute(d.Engine)
button.MoveTo(render.Point{
X: (d.width / 2) - (button.Size().W / 2),
Y: 260,
})
button.Present(d.Engine)
s.Supervisor.Present(d.Engine)
return nil
}

View File

@ -124,13 +124,11 @@ func (r *Renderer) Poll() (*events.State, error) {
// Is a mouse button pressed down?
if t.Button == 1 {
var eventName string
if DebugClickEvents {
if t.State == 1 && s.Button1.Now == false {
eventName = "DOWN"
} else if t.State == 0 && s.Button1.Now == true {
eventName = "UP"
}
}
if eventName != "" {
log.Debug("tick:%d Mouse Button1 %s BEFORE: %+v",

View File

@ -7,7 +7,12 @@ import (
// ColorToSDL converts Doodle's Color type to an sdl.Color.
func ColorToSDL(c render.Color) sdl.Color {
return sdl.Color{c.Red, c.Green, c.Blue, c.Alpha}
return sdl.Color{
R: c.Red,
G: c.Green,
B: c.Blue,
A: c.Alpha,
}
}
// RectToSDL converts Doodle's Rect type to an sdl.Rect.

View File

@ -18,11 +18,15 @@ type Button struct {
HighlightColor render.Color
ShadowColor render.Color
OutlineColor render.Color
// Private options.
hovering bool
clicked bool
}
// NewButton creates a new Button.
func NewButton(label Label) *Button {
return &Button{
w := &Button{
Label: label,
Padding: 4, // TODO magic number
Border: 2,
@ -34,6 +38,22 @@ func NewButton(label Label) *Button {
ShadowColor: theme.ButtonShadowColor,
OutlineColor: theme.ButtonOutlineColor,
}
w.Handle("MouseOver", func(p render.Point) {
w.hovering = true
})
w.Handle("MouseOut", func(p render.Point) {
w.hovering = false
})
w.Handle("MouseDown", func(p render.Point) {
w.clicked = true
})
w.Handle("MouseUp", func(p render.Point) {
w.clicked = false
})
return w
}
// SetText quickly changes the text of the label.
@ -74,7 +94,11 @@ func (w *Button) Present(e render.Engine) {
})
// Highlight on the top left edge.
e.DrawBox(w.HighlightColor, box)
color := w.HighlightColor
if w.clicked {
color = w.ShadowColor
}
e.DrawBox(color, box)
box.W = S.W
// Shadow on the bottom right edge.
@ -82,12 +106,20 @@ func (w *Button) Present(e render.Engine) {
box.Y += w.Border
box.W -= w.Border
box.H -= w.Border
e.DrawBox(w.ShadowColor, box)
color = w.ShadowColor
if w.clicked {
color = w.HighlightColor
}
e.DrawBox(color, box)
// Background color of the button.
box.W -= w.Border
box.H -= w.Border
if w.hovering {
e.DrawBox(render.Yellow, box)
} else {
e.DrawBox(w.Background, box)
}
// Draw the text label inside.
w.Label.MoveTo(render.Point{

96
ui/supervisor.go Normal file
View File

@ -0,0 +1,96 @@
package ui
import (
"sync"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render"
)
// Supervisor keeps track of widgets of interest to notify them about
// interaction events such as mouse hovers and clicks in their general
// vicinity.
type Supervisor struct {
lock sync.RWMutex
widgets []Widget
hovering map[int]interface{}
clicked map[int]interface{}
}
// NewSupervisor creates a supervisor.
func NewSupervisor() *Supervisor {
return &Supervisor{
hovering: map[int]interface{}{},
clicked: map[int]interface{}{},
}
}
// Loop to check events and pass them to managed widgets.
func (s *Supervisor) Loop(ev *events.State) {
var (
XY = render.Point{
X: ev.CursorX.Now,
Y: ev.CursorY.Now,
}
)
// See if we are hovering over any widgets.
for id, w := range s.widgets {
var (
P = w.Point()
S = w.Size()
P2 = render.Point{
X: P.X + S.W,
Y: P.Y + S.H,
}
)
if XY.X >= P.X && XY.X <= P2.X && XY.Y >= P.Y && XY.Y <= P2.Y {
// Cursor has intersected the widget.
if _, ok := s.hovering[id]; !ok {
w.Event("MouseOver", XY)
s.hovering[id] = nil
}
_, isClicked := s.clicked[id]
if ev.Button1.Now {
if !isClicked {
w.Event("MouseDown", XY)
s.clicked[id] = nil
}
} else if isClicked {
w.Event("MouseUp", XY)
w.Event("Click", XY)
delete(s.clicked, id)
}
} else {
// Cursor is not intersecting the widget.
if _, ok := s.hovering[id]; ok {
w.Event("MouseOut", XY)
delete(s.hovering, id)
}
if _, ok := s.clicked[id]; ok {
w.Event("MouseUp", XY)
delete(s.clicked, id)
}
}
}
}
// Present all widgets managed by the supervisor.
func (s *Supervisor) Present(e render.Engine) {
s.lock.RLock()
defer s.lock.RUnlock()
for _, w := range s.widgets {
w.Present(e)
}
}
// Add a widget to be supervised.
func (s *Supervisor) Add(w Widget) {
s.lock.Lock()
s.widgets = append(s.widgets, w)
s.lock.Unlock()
}

View File

@ -4,16 +4,15 @@ import "git.kirsle.net/apps/doodle/render"
// Widget is a user interface element.
type Widget interface {
Width() int32 // Get width
Height() int32 // Get height
SetWidth(int32) // Set
SetHeight(int32) // Set
Point() render.Point
MoveTo(render.Point)
MoveBy(render.Point)
Size() render.Rect // Return the Width and Height of the widget.
Resize(render.Rect)
Handle(string, func(render.Point))
Event(string, render.Point) // called internally to trigger an event
// Run any render computations; by the end the widget must know its
// Width and Height. For example the Label widget will render itself onto
// an SDL Surface and then it will know its bounding box, but not before.
@ -29,6 +28,7 @@ type BaseWidget struct {
width int32
height int32
point render.Point
handlers map[string][]func(render.Point)
}
// Point returns the X,Y position of the widget on the window.
@ -61,3 +61,28 @@ func (w *BaseWidget) Resize(v render.Rect) {
w.width = v.W
w.height = v.H
}
// Event is called internally by Doodle to trigger an event.
func (w *BaseWidget) Event(name string, p render.Point) {
if handlers, ok := w.handlers[name]; ok {
for _, fn := range handlers {
fn(p)
}
}
}
// Handle an event in the widget.
func (w *BaseWidget) Handle(name string, fn func(render.Point)) {
if w.handlers == nil {
w.handlers = map[string][]func(render.Point){}
}
if _, ok := w.handlers[name]; !ok {
w.handlers[name] = []func(render.Point){}
}
w.handlers[name] = append(w.handlers[name], fn)
}
// OnMouseOut should be overridden on widgets who want this event.
func (w *BaseWidget) OnMouseOut(render.Point) {}