Added mouse handling

This commit is contained in:
Chris Miller 2019-11-04 05:34:46 +00:00 committed by Trevor Slocum
parent 6a97e2648e
commit 74b2573bf9
10 changed files with 235 additions and 5 deletions

View File

@ -42,6 +42,9 @@ type Application struct {
// Whether or not the application resizes the root primitive.
rootFullscreen bool
// Whether or not to enable mouse events.
enableMouse bool
// 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
// be forwarded).
@ -76,6 +79,33 @@ type Application struct {
// (screen.Init() and draw() will be called implicitly). A value of nil will
// stop the application.
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.
@ -107,6 +137,21 @@ func (a *Application) GetInputCapture() func(event *tcell.EventKey) *tcell.Event
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
// applications, this is not needed and you should be familiar with
// tcell.Screen when using this function.
@ -135,6 +180,13 @@ func (a *Application) SetScreen(screen tcell.Screen) *Application {
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
// when Stop() was called.
func (a *Application) Run() error {
@ -152,6 +204,9 @@ func (a *Application) Run() error {
a.Unlock()
return err
}
if a.enableMouse {
a.screen.EnableMouse()
}
}
// We catch panics to clean up because they mess up the terminal.
@ -221,13 +276,15 @@ EventLoop:
break EventLoop
}
a.RLock()
p := a.focus
inputCapture := a.inputCapture
mouseCapture := a.mouseCapture
screen := a.screen
a.RUnlock()
switch event := event.(type) {
case *tcell.EventKey:
a.RLock()
p := a.focus
inputCapture := a.inputCapture
a.RUnlock()
// Intercept keys.
if inputCapture != nil {
event = inputCapture(event)
@ -287,6 +344,27 @@ EventLoop:
}
screen.Clear()
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.
@ -302,6 +380,34 @@ EventLoop:
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.
func (a *Application) Stop() {
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.
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.
@ -192,6 +197,45 @@ func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey {
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.
func (b *Box) SetBackgroundColor(color tcell.Color) *Box {
b.backgroundColor = color
@ -358,3 +402,8 @@ func (b *Box) HasFocus() bool {
func (b *Box) GetFocusable() Focusable {
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
}
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

@ -606,3 +606,17 @@ func (f *Form) focusIndex() int {
}
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
}
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

@ -175,3 +175,7 @@ func (m *Modal) Draw(screen tcell.Screen) {
m.frame.SetRect(x, y, width, height)
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)
}
}
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() 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)
}