From 602273aa16f7ab8cda121c83c002a265cf5faaf1 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Wed, 25 Jul 2018 20:25:02 -0700 Subject: [PATCH] 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. --- main_scene.go | 54 +++++++++++++++---------- render/sdl/sdl.go | 10 ++--- render/sdl/utils.go | 7 +++- ui/button.go | 40 +++++++++++++++++-- ui/supervisor.go | 96 +++++++++++++++++++++++++++++++++++++++++++++ ui/widget.go | 39 ++++++++++++++---- 6 files changed, 208 insertions(+), 38 deletions(-) create mode 100644 ui/supervisor.go diff --git a/main_scene.go b/main_scene.go index fe40071..b4bb589 100644 --- a/main_scene.go +++ b/main_scene.go @@ -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 } diff --git a/render/sdl/sdl.go b/render/sdl/sdl.go index 71872d8..5338e9e 100644 --- a/render/sdl/sdl.go +++ b/render/sdl/sdl.go @@ -124,12 +124,10 @@ 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 t.State == 1 && s.Button1.Now == false { + eventName = "DOWN" + } else if t.State == 0 && s.Button1.Now == true { + eventName = "UP" } if eventName != "" { diff --git a/render/sdl/utils.go b/render/sdl/utils.go index 9085e31..efcbf9c 100644 --- a/render/sdl/utils.go +++ b/render/sdl/utils.go @@ -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. diff --git a/ui/button.go b/ui/button.go index abf5313..b68f288 100644 --- a/ui/button.go +++ b/ui/button.go @@ -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 - e.DrawBox(w.Background, box) + if w.hovering { + e.DrawBox(render.Yellow, box) + } else { + e.DrawBox(w.Background, box) + } // Draw the text label inside. w.Label.MoveTo(render.Point{ diff --git a/ui/supervisor.go b/ui/supervisor.go new file mode 100644 index 0000000..0f6d93f --- /dev/null +++ b/ui/supervisor.go @@ -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() +} diff --git a/ui/widget.go b/ui/widget.go index d4cbaaa..947e681 100644 --- a/ui/widget.go +++ b/ui/widget.go @@ -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. @@ -26,9 +25,10 @@ type Widget interface { // BaseWidget holds common functionality for all widgets, such as managing // their widths and heights. type BaseWidget struct { - width int32 - height int32 - point render.Point + 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) {}