Added mouse handling

This commit is contained in:
Chris Miller 2019-11-04 05:34:46 +00:00
parent 685bf6da76
commit 96875c75b9
10 changed files with 236 additions and 8 deletions

View File

@ -38,6 +38,9 @@ type Application struct {
// Whether or not the application resizes the root primitive. // Whether or not the application resizes the root primitive.
rootFullscreen bool rootFullscreen bool
// Enable mouse events?
enableMouse bool
// An optional capture function which receives a key event and returns the // An optional capture function which receives a key event and returns the
// event to be forwarded to the default input handler (nil if nothing should // event to be forwarded to the default input handler (nil if nothing should
// be forwarded). // be forwarded).
@ -62,6 +65,33 @@ type Application struct {
// (screen.Init() and draw() will be called implicitly). A value of nil will // (screen.Init() and draw() will be called implicitly). A value of nil will
// stop the application. // stop the application.
screenReplacement chan tcell.Screen screenReplacement chan tcell.Screen
// An optional capture function which receives a mouse event and returns the
// event to be forwarded to the default mouse handler (nil if nothing should
// be forwarded).
mouseCapture func(event EventMouse) EventMouse
}
// EventKey is the key input event info.
// This exists for some consistency with EventMouse,
// even though it's just an alias to *tcell.EventKey for backwards compatibility.
type EventKey = *tcell.EventKey
// EventMouse is the mouse event info.
type EventMouse struct {
*tcell.EventMouse
Target Primitive
Application *Application
}
// IsZero returns true if this is a zero object.
func (e EventMouse) IsZero() bool {
return e == EventMouse{}
}
// SetFocus will set focus to the primitive.
func (e EventMouse) SetFocus(p Primitive) {
e.Application.SetFocus(p)
} }
// NewApplication creates and returns a new application. // NewApplication creates and returns a new application.
@ -93,6 +123,22 @@ func (a *Application) GetInputCapture() func(event *tcell.EventKey) *tcell.Event
return a.inputCapture return a.inputCapture
} }
// SetMouseCapture sets a function which captures mouse events before they are
// forwarded to the appropriate mouse event handler.
// This function can then choose to forward that event (or a
// different one) by returning it or stop the event processing by returning
// nil.
func (a *Application) SetMouseCapture(capture func(event EventMouse) EventMouse) *Application {
a.mouseCapture = capture
return a
}
// GetMouseCapture returns the function installed with SetMouseCapture() or nil
// if no such function has been installed.
func (a *Application) GetMouseCapture() func(event EventMouse) EventMouse {
return a.mouseCapture
}
// SetScreen allows you to provide your own tcell.Screen object. For most // SetScreen allows you to provide your own tcell.Screen object. For most
// applications, this is not needed and you should be familiar with // applications, this is not needed and you should be familiar with
// tcell.Screen when using this function. // tcell.Screen when using this function.
@ -121,6 +167,13 @@ func (a *Application) SetScreen(screen tcell.Screen) *Application {
return a return a
} }
// EnableMouse enables mouse events.
func (a *Application) EnableMouse() {
a.Lock()
a.enableMouse = true
a.Unlock()
}
// Run starts the application and thus the event loop. This function returns // Run starts the application and thus the event loop. This function returns
// when Stop() was called. // when Stop() was called.
func (a *Application) Run() error { func (a *Application) Run() error {
@ -138,6 +191,9 @@ func (a *Application) Run() error {
a.Unlock() a.Unlock()
return err return err
} }
if a.enableMouse {
a.screen.EnableMouse()
}
} }
// We catch panics to clean up because they mess up the terminal. // We catch panics to clean up because they mess up the terminal.
@ -207,13 +263,15 @@ EventLoop:
break EventLoop break EventLoop
} }
a.RLock()
p := a.focus
inputCapture := a.inputCapture
mouseCapture := a.mouseCapture
screen := a.screen
a.RUnlock()
switch event := event.(type) { switch event := event.(type) {
case *tcell.EventKey: case *tcell.EventKey:
a.RLock()
p := a.focus
inputCapture := a.inputCapture
a.RUnlock()
// Intercept keys. // Intercept keys.
if inputCapture != nil { if inputCapture != nil {
event = inputCapture(event) event = inputCapture(event)
@ -238,14 +296,32 @@ EventLoop:
} }
} }
case *tcell.EventResize: case *tcell.EventResize:
a.RLock()
screen := a.screen
a.RUnlock()
if screen == nil { if screen == nil {
continue continue
} }
screen.Clear() screen.Clear()
a.draw() a.draw()
case *tcell.EventMouse:
atX, atY := event.Position()
ptarget := a.GetPrimitiveAtPoint(atX, atY) // p under mouse.
if ptarget == nil {
ptarget = p // Fallback to focused.
}
event2 := EventMouse{event, ptarget, a}
// Intercept event.
if mouseCapture != nil {
event2 = mouseCapture(event2)
if event2.IsZero() {
a.draw()
continue // Don't forward event.
}
}
if handler := ptarget.MouseHandler(); handler != nil {
handler(event2)
a.draw()
}
} }
// If we have updates, now is the time to execute them. // If we have updates, now is the time to execute them.
@ -261,6 +337,34 @@ EventLoop:
return nil return nil
} }
func findAtPoint(atX, atY int, p Primitive) Primitive {
x, y, w, h := p.GetRect()
if atX < x || atY < y {
return nil
}
if atX >= x+w || atY >= y+h {
return nil
}
bestp := p
for _, pchild := range p.GetChildren() {
x := findAtPoint(atX, atY, pchild)
if x != nil {
// Always overwrite if we find another one,
// this is because if any overlap, the last one is "on top".
bestp = x
}
}
return bestp
}
// GetPrimitiveAtPoint returns the Primitive at the specified point, or nil.
// Note that this only works with a valid hierarchy of primitives (children)
func (a *Application) GetPrimitiveAtPoint(atX, atY int) Primitive {
a.RLock()
defer a.RUnlock()
return findAtPoint(atX, atY, a.root)
}
// Stop stops the application, causing Run() to return. // Stop stops the application, causing Run() to return.
func (a *Application) Stop() { func (a *Application) Stop() {
a.Lock() a.Lock()

49
box.go
View File

@ -58,6 +58,11 @@ type Box struct {
// An optional function which is called before the box is drawn. // An optional function which is called before the box is drawn.
draw func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) draw func(screen tcell.Screen, x, y, width, height int) (int, int, int, int)
// An optional capture function which receives a mouse event and returns the
// event to be forwarded to the primitive's default mouse event handler (nil if
// nothing should be forwarded).
mouseCapture func(event EventMouse) EventMouse
} }
// NewBox returns a Box without a border. // NewBox returns a Box without a border.
@ -192,6 +197,45 @@ func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey {
return b.inputCapture return b.inputCapture
} }
// WrapMouseHandler wraps a mouse event handler (see MouseHandler()) with the
// functionality to capture input (see SetMouseCapture()) before passing it
// on to the provided (default) event handler.
//
// This is only meant to be used by subclassing primitives.
func (b *Box) WrapMouseHandler(mouseHandler func(EventMouse)) func(EventMouse) {
return func(event EventMouse) {
if b.mouseCapture != nil {
event = b.mouseCapture(event)
}
if !event.IsZero() && mouseHandler != nil {
mouseHandler(event)
}
}
}
// MouseHandler returns nil.
func (b *Box) MouseHandler() func(event EventMouse) {
return b.WrapMouseHandler(nil)
}
// SetMouseCapture installs a function which captures events before they are
// forwarded to the primitive's default event handler. This function can
// then choose to forward that event (or a different one) to the default
// handler by returning it. If nil is returned, the default handler will not
// be called.
//
// Providing a nil handler will remove a previously existing handler.
func (b *Box) SetMouseCapture(capture func(EventMouse) EventMouse) *Box {
b.mouseCapture = capture
return b
}
// GetMouseCapture returns the function installed with SetMouseCapture() or nil
// if no such function has been installed.
func (b *Box) GetMouseCapture() func(EventMouse) EventMouse {
return b.mouseCapture
}
// SetBackgroundColor sets the box's background color. // SetBackgroundColor sets the box's background color.
func (b *Box) SetBackgroundColor(color tcell.Color) *Box { func (b *Box) SetBackgroundColor(color tcell.Color) *Box {
b.backgroundColor = color b.backgroundColor = color
@ -353,3 +397,8 @@ func (b *Box) HasFocus() bool {
func (b *Box) GetFocusable() Focusable { func (b *Box) GetFocusable() Focusable {
return b.focus return b.focus
} }
// GetChildren gets the children.
func (b *Box) GetChildren() []Primitive {
return nil
}

View File

@ -135,3 +135,15 @@ func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Prim
} }
}) })
} }
// InputHandler returns the handler for this primitive.
func (b *Button) MouseHandler() func(event EventMouse) {
return b.WrapMouseHandler(func(event EventMouse) {
// Process mouse event.
if event.Buttons()&tcell.Button1 != 0 {
if b.selected != nil {
b.selected()
}
}
})
}

View File

@ -195,3 +195,11 @@ func (f *Flex) HasFocus() bool {
} }
return false return false
} }
func (f *Flex) GetChildren() []Primitive {
children := make([]Primitive, len(f.items))
for i, item := range f.items {
children[i] = item.Item
}
return children
}

14
form.go
View File

@ -600,3 +600,17 @@ func (f *Form) focusIndex() int {
} }
return -1 return -1
} }
func (f *Form) GetChildren() []Primitive {
children := make([]Primitive, len(f.items)+len(f.buttons))
i := 0
for _, item := range f.items {
children[i] = item
i++
}
for _, button := range f.buttons {
children[i] = button
i++
}
return children
}

View File

@ -155,3 +155,7 @@ func (f *Frame) HasFocus() bool {
} }
return false return false
} }
func (f *Frame) GetChildren() []Primitive {
return []Primitive{f.primitive}
}

View File

@ -660,3 +660,11 @@ func (g *Grid) Draw(screen tcell.Screen) {
} }
} }
} }
func (g *Grid) GetChildren() []Primitive {
children := make([]Primitive, len(g.items))
for i, item := range g.items {
children[i] = item.Item
}
return children
}

View File

@ -169,3 +169,7 @@ func (m *Modal) Draw(screen tcell.Screen) {
m.frame.SetRect(x, y, width, height) m.frame.SetRect(x, y, width, height)
m.frame.Draw(screen) m.frame.Draw(screen)
} }
func (m *Modal) GetChildren() []Primitive {
return []Primitive{m.frame}
}

View File

@ -278,3 +278,15 @@ func (p *Pages) Draw(screen tcell.Screen) {
page.Item.Draw(screen) page.Item.Draw(screen)
} }
} }
func (p *Pages) GetChildren() []Primitive {
var children []Primitive
for _, page := range p.pages {
// Considering invisible pages as not children.
// Even though we track all the pages, not all are "children" currently.
if page.Visible {
children = append(children, page.Item)
}
}
return children
}

View File

@ -43,4 +43,17 @@ type Primitive interface {
// GetFocusable returns the item's Focusable. // GetFocusable returns the item's Focusable.
GetFocusable() Focusable GetFocusable() Focusable
// GetChildren gets the children.
GetChildren() []Primitive
// MouseHandler returns a handler which receives mouse events.
// It is called by the Application class.
//
// A zero value of EventMouse{} may also be returned to stop propagation.
//
// The Box class provides functionality to intercept mouse events. If you
// subclass from Box, it is recommended that you wrap your handler using
// Box.WrapMouseHandler() so you inherit that functionality.
MouseHandler() func(event EventMouse)
} }