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:
parent
41e1838549
commit
602273aa16
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
// MainScene implements the main menu of Doodle.
|
// MainScene implements the main menu of Doodle.
|
||||||
type MainScene struct {
|
type MainScene struct {
|
||||||
|
Supervisor *ui.Supervisor
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name of the scene.
|
// Name of the scene.
|
||||||
|
@ -17,11 +18,43 @@ func (s *MainScene) Name() string {
|
||||||
|
|
||||||
// Setup the scene.
|
// Setup the scene.
|
||||||
func (s *MainScene) Setup(d *Doodle) error {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop the editor scene.
|
// Loop the editor scene.
|
||||||
func (s *MainScene) Loop(d *Doodle, ev *events.State) error {
|
func (s *MainScene) Loop(d *Doodle, ev *events.State) error {
|
||||||
|
s.Supervisor.Loop(ev)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,26 +77,7 @@ func (s *MainScene) Draw(d *Doodle) error {
|
||||||
})
|
})
|
||||||
label.Present(d.Engine)
|
label.Present(d.Engine)
|
||||||
|
|
||||||
button := ui.NewButton(*ui.NewLabel(render.Text{
|
s.Supervisor.Present(d.Engine)
|
||||||
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)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,12 +124,10 @@ func (r *Renderer) Poll() (*events.State, error) {
|
||||||
// Is a mouse button pressed down?
|
// Is a mouse button pressed down?
|
||||||
if t.Button == 1 {
|
if t.Button == 1 {
|
||||||
var eventName string
|
var eventName string
|
||||||
if DebugClickEvents {
|
if t.State == 1 && s.Button1.Now == false {
|
||||||
if t.State == 1 && s.Button1.Now == false {
|
eventName = "DOWN"
|
||||||
eventName = "DOWN"
|
} else if t.State == 0 && s.Button1.Now == true {
|
||||||
} else if t.State == 0 && s.Button1.Now == true {
|
eventName = "UP"
|
||||||
eventName = "UP"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if eventName != "" {
|
if eventName != "" {
|
||||||
|
|
|
@ -7,7 +7,12 @@ import (
|
||||||
|
|
||||||
// ColorToSDL converts Doodle's Color type to an sdl.Color.
|
// ColorToSDL converts Doodle's Color type to an sdl.Color.
|
||||||
func ColorToSDL(c render.Color) 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.
|
// RectToSDL converts Doodle's Rect type to an sdl.Rect.
|
||||||
|
|
40
ui/button.go
40
ui/button.go
|
@ -18,11 +18,15 @@ type Button struct {
|
||||||
HighlightColor render.Color
|
HighlightColor render.Color
|
||||||
ShadowColor render.Color
|
ShadowColor render.Color
|
||||||
OutlineColor render.Color
|
OutlineColor render.Color
|
||||||
|
|
||||||
|
// Private options.
|
||||||
|
hovering bool
|
||||||
|
clicked bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewButton creates a new Button.
|
// NewButton creates a new Button.
|
||||||
func NewButton(label Label) *Button {
|
func NewButton(label Label) *Button {
|
||||||
return &Button{
|
w := &Button{
|
||||||
Label: label,
|
Label: label,
|
||||||
Padding: 4, // TODO magic number
|
Padding: 4, // TODO magic number
|
||||||
Border: 2,
|
Border: 2,
|
||||||
|
@ -34,6 +38,22 @@ func NewButton(label Label) *Button {
|
||||||
ShadowColor: theme.ButtonShadowColor,
|
ShadowColor: theme.ButtonShadowColor,
|
||||||
OutlineColor: theme.ButtonOutlineColor,
|
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.
|
// 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.
|
// 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
|
box.W = S.W
|
||||||
|
|
||||||
// Shadow on the bottom right edge.
|
// Shadow on the bottom right edge.
|
||||||
|
@ -82,12 +106,20 @@ func (w *Button) Present(e render.Engine) {
|
||||||
box.Y += w.Border
|
box.Y += w.Border
|
||||||
box.W -= w.Border
|
box.W -= w.Border
|
||||||
box.H -= 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.
|
// Background color of the button.
|
||||||
box.W -= w.Border
|
box.W -= w.Border
|
||||||
box.H -= w.Border
|
box.H -= w.Border
|
||||||
e.DrawBox(w.Background, box)
|
if w.hovering {
|
||||||
|
e.DrawBox(render.Yellow, box)
|
||||||
|
} else {
|
||||||
|
e.DrawBox(w.Background, box)
|
||||||
|
}
|
||||||
|
|
||||||
// Draw the text label inside.
|
// Draw the text label inside.
|
||||||
w.Label.MoveTo(render.Point{
|
w.Label.MoveTo(render.Point{
|
||||||
|
|
96
ui/supervisor.go
Normal file
96
ui/supervisor.go
Normal 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()
|
||||||
|
}
|
39
ui/widget.go
39
ui/widget.go
|
@ -4,16 +4,15 @@ import "git.kirsle.net/apps/doodle/render"
|
||||||
|
|
||||||
// Widget is a user interface element.
|
// Widget is a user interface element.
|
||||||
type Widget interface {
|
type Widget interface {
|
||||||
Width() int32 // Get width
|
|
||||||
Height() int32 // Get height
|
|
||||||
SetWidth(int32) // Set
|
|
||||||
SetHeight(int32) // Set
|
|
||||||
Point() render.Point
|
Point() render.Point
|
||||||
MoveTo(render.Point)
|
MoveTo(render.Point)
|
||||||
MoveBy(render.Point)
|
MoveBy(render.Point)
|
||||||
Size() render.Rect // Return the Width and Height of the widget.
|
Size() render.Rect // Return the Width and Height of the widget.
|
||||||
Resize(render.Rect)
|
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
|
// Run any render computations; by the end the widget must know its
|
||||||
// Width and Height. For example the Label widget will render itself onto
|
// 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.
|
// an SDL Surface and then it will know its bounding box, but not before.
|
||||||
|
@ -26,9 +25,10 @@ type Widget interface {
|
||||||
// BaseWidget holds common functionality for all widgets, such as managing
|
// BaseWidget holds common functionality for all widgets, such as managing
|
||||||
// their widths and heights.
|
// their widths and heights.
|
||||||
type BaseWidget struct {
|
type BaseWidget struct {
|
||||||
width int32
|
width int32
|
||||||
height int32
|
height int32
|
||||||
point render.Point
|
point render.Point
|
||||||
|
handlers map[string][]func(render.Point)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Point returns the X,Y position of the widget on the window.
|
// 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.width = v.W
|
||||||
w.height = v.H
|
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) {}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user