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) {}