Noah Petherbridge
cc1e441232
* Implement Brush Sizes for drawtool.Stroke and add a UI to the tools panel to control the brush size. * Brush sizes: 1, 2, 4, 8, 16, 24, 32, 48, 64 * Add the Eraser Tool to editor mode. It uses a default brush size of 16 and a max size of 32 due to some performance issues. * The Undo/Redo system now remembers the original color of pixels when you change them, so that Undo will set them back how they were instead of deleting the pixel entirely. Due to performance issues, this only happens when your Brush Size is 0 (drawing single-pixel shapes). * UI: Add an IntVariable option to ui.Label to bind showing the value of an int reference. Aforementioned performance issues: * When we try to remember whole rects of pixels for drawing thick shapes, it requires a ton of scanning for each step of the shape. Even de-duplicating pixel checks, tons of extra reads are constantly checked. * The Eraser is the only tool that absolutely needs to be able to remember wiped pixels AND have large brush sizes. The performance sucks and lags a bit if you erase a lot all at once, but it's a trade-off for now. * So pixels aren't remembered when drawing lines in your level with thick brushes, so the Undo action will simply delete your pixels and not reset them. Only the Eraser can bring back pixels.
223 lines
4.8 KiB
Go
223 lines
4.8 KiB
Go
package ui
|
|
|
|
import (
|
|
"errors"
|
|
"sync"
|
|
|
|
"git.kirsle.net/apps/doodle/lib/events"
|
|
"git.kirsle.net/apps/doodle/lib/render"
|
|
)
|
|
|
|
// Event is a named event that the supervisor will send.
|
|
type Event int
|
|
|
|
// Events.
|
|
const (
|
|
NullEvent Event = iota
|
|
MouseOver
|
|
MouseOut
|
|
MouseDown
|
|
MouseUp
|
|
Click
|
|
KeyDown
|
|
KeyUp
|
|
KeyPress
|
|
Drop
|
|
)
|
|
|
|
// 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
|
|
serial int // ID number of each widget added in order
|
|
widgets map[int]WidgetSlot // map of widget ID to WidgetSlot
|
|
hovering map[int]interface{} // map of widgets under the cursor
|
|
clicked map[int]interface{} // map of widgets being clicked
|
|
dd *DragDrop
|
|
}
|
|
|
|
// WidgetSlot holds a widget with a unique ID number in a sorted list.
|
|
type WidgetSlot struct {
|
|
id int
|
|
widget Widget
|
|
}
|
|
|
|
// NewSupervisor creates a supervisor.
|
|
func NewSupervisor() *Supervisor {
|
|
return &Supervisor{
|
|
widgets: map[int]WidgetSlot{},
|
|
hovering: map[int]interface{}{},
|
|
clicked: map[int]interface{}{},
|
|
dd: NewDragDrop(),
|
|
}
|
|
}
|
|
|
|
// DragStart sets the drag state.
|
|
func (s *Supervisor) DragStart() {
|
|
s.dd.Start()
|
|
}
|
|
|
|
// DragStop stops the drag state.
|
|
func (s *Supervisor) DragStop() {
|
|
s.dd.Stop()
|
|
}
|
|
|
|
// IsDragging returns whether the drag state is enabled.
|
|
func (s *Supervisor) IsDragging() bool {
|
|
return s.dd.IsDragging()
|
|
}
|
|
|
|
// Error messages that may be returned by Supervisor.Loop()
|
|
var (
|
|
// The caller should STOP forwarding any mouse or keyboard events to any
|
|
// other handles for the remainder of this tick.
|
|
ErrStopPropagation = errors.New("stop all event propagation")
|
|
)
|
|
|
|
// Loop to check events and pass them to managed widgets.
|
|
//
|
|
// Useful errors returned by this may be:
|
|
// - ErrStopPropagation
|
|
func (s *Supervisor) Loop(ev *events.State) error {
|
|
var (
|
|
XY = render.Point{
|
|
X: ev.CursorX.Now,
|
|
Y: ev.CursorY.Now,
|
|
}
|
|
)
|
|
|
|
// See if we are hovering over any widgets.
|
|
hovering, outside := s.Hovering(XY)
|
|
|
|
// If we are dragging something around, do not trigger any mouse events
|
|
// to other widgets but DO notify any widget we dropped on top of!
|
|
if s.dd.IsDragging() {
|
|
if !ev.Button1.Now && !ev.Button2.Now {
|
|
// The mouse has been released. TODO: make mouse button important?
|
|
for _, child := range hovering {
|
|
child.widget.Event(Drop, XY)
|
|
}
|
|
s.DragStop()
|
|
}
|
|
return ErrStopPropagation
|
|
}
|
|
|
|
for _, child := range hovering {
|
|
var (
|
|
id = child.id
|
|
w = child.widget
|
|
)
|
|
if w.Hidden() {
|
|
// TODO: somehow the Supervisor wasn't triggering hidden widgets
|
|
// anyway, but I don't know why. Adding this check for safety.
|
|
continue
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
for _, child := range outside {
|
|
var (
|
|
id = child.id
|
|
w = child.widget
|
|
)
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Hovering returns all of the widgets managed by Supervisor that are under
|
|
// the mouse cursor. Returns the set of widgets below the cursor and the set
|
|
// of widgets not below the cursor.
|
|
func (s *Supervisor) Hovering(cursor render.Point) (hovering, outside []WidgetSlot) {
|
|
var XY = cursor // for shorthand
|
|
hovering = []WidgetSlot{}
|
|
outside = []WidgetSlot{}
|
|
|
|
// Check all the widgets under our care.
|
|
for child := range s.Widgets() {
|
|
var (
|
|
w = child.widget
|
|
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 intersects the widget.
|
|
hovering = append(hovering, child)
|
|
} else {
|
|
outside = append(outside, child)
|
|
}
|
|
}
|
|
|
|
return hovering, outside
|
|
}
|
|
|
|
// Widgets returns a channel of widgets managed by the supervisor in the order
|
|
// they were added.
|
|
func (s *Supervisor) Widgets() <-chan WidgetSlot {
|
|
pipe := make(chan WidgetSlot)
|
|
go func() {
|
|
for i := 0; i < s.serial; i++ {
|
|
if w, ok := s.widgets[i]; ok {
|
|
pipe <- w
|
|
}
|
|
}
|
|
close(pipe)
|
|
}()
|
|
return pipe
|
|
}
|
|
|
|
// Present all widgets managed by the supervisor.
|
|
func (s *Supervisor) Present(e render.Engine) {
|
|
s.lock.RLock()
|
|
defer s.lock.RUnlock()
|
|
|
|
for child := range s.Widgets() {
|
|
var w = child.widget
|
|
w.Present(e, w.Point())
|
|
}
|
|
}
|
|
|
|
// Add a widget to be supervised.
|
|
func (s *Supervisor) Add(w Widget) {
|
|
s.lock.Lock()
|
|
s.widgets[s.serial] = WidgetSlot{
|
|
id: s.serial,
|
|
widget: w,
|
|
}
|
|
s.serial++
|
|
s.lock.Unlock()
|
|
}
|