Merge pull request #363 from millerlogic/mouse-support

Mouse support
pull/422/head
rivo 4 years ago committed by GitHub
commit 160d8fda1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -52,6 +52,9 @@ type Application struct {
// Whether or not the application resizes the root primitive.
rootFullscreen bool
// 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,19 @@ 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 *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction)
// An optional mouse capture Primitive returned from the MouseHandler.
mouseHandlerCapture Primitive
lastMouseX, lastMouseY int // track last mouse pos
mouseDownX, mouseDownY int // track last mouse down pos
lastClickTime time.Time
lastMouseBtn tcell.ButtonMask
}
// NewApplication creates and returns a new application.
@ -107,6 +123,22 @@ 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 *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction)) *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 *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction) {
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 +167,21 @@ func (a *Application) SetScreen(screen tcell.Screen) *Application {
return a
}
// EnableMouse enables mouse events.
func (a *Application) EnableMouse(enable bool) *Application {
a.Lock()
defer a.Unlock()
if enable != a.enableMouse && a.screen != nil {
if enable {
a.screen.EnableMouse()
} else {
a.screen.DisableMouse()
}
}
a.enableMouse = enable
return a
}
// Run starts the application and thus the event loop. This function returns
// when Stop() was called.
func (a *Application) Run() error {
@ -157,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.
@ -279,6 +329,16 @@ EventLoop:
lastRedraw = time.Now()
screen.Clear()
a.draw()
case *tcell.EventMouse:
consumed, isMouseDownAction := a.fireMouseActions(event)
if consumed {
a.draw()
}
// Keep state:
a.lastMouseBtn = event.Buttons()
if isMouseDownAction {
a.mouseDownX, a.mouseDownY = event.Position()
}
}
// If we have updates, now is the time to execute them.
@ -295,6 +355,103 @@ EventLoop:
return nil
}
// fireMouseActions determines each mouse action from mouse events
// and fires the appropriate mouse handlers and mouse captures.
func (a *Application) fireMouseActions(event *tcell.EventMouse) (consumed, isMouseDownAction bool) {
a.RLock()
root := a.root
mouseCapture := a.mouseCapture
a.RUnlock()
// Fire a mouse action.
mouseEv := func(action MouseAction) {
switch action {
case MouseLeftDown, MouseMiddleDown, MouseRightDown:
isMouseDownAction = true
}
// Intercept event.
if mouseCapture != nil {
event, action = mouseCapture(event, action)
if event == nil {
consumed = true
return // Don't forward event.
}
}
var handlerTarget Primitive
if a.mouseHandlerCapture != nil { // Check if already captured.
handlerTarget = a.mouseHandlerCapture
} else {
handlerTarget = root
}
var newHandlerCapture Primitive // None by default.
if handlerTarget != nil {
if handler := handlerTarget.MouseHandler(); handler != nil {
hconsumed := false
hconsumed, newHandlerCapture = handler(action, event, func(p Primitive) {
a.SetFocus(p)
})
if hconsumed {
consumed = true
}
}
}
a.mouseHandlerCapture = newHandlerCapture
}
atX, atY := event.Position()
ebuttons := event.Buttons()
clickMoved := atX != a.mouseDownX || atY != a.mouseDownY
btnDiff := ebuttons ^ a.lastMouseBtn
if atX != a.lastMouseX || atY != a.lastMouseY {
mouseEv(MouseMove)
a.lastMouseX = atX
a.lastMouseY = atY
}
for _, x := range []struct {
button tcell.ButtonMask
down, up, click, dclick MouseAction
}{
{tcell.Button1, MouseLeftDown, MouseLeftUp, MouseLeftClick, MouseLeftDoubleClick},
{tcell.Button2, MouseMiddleDown, MouseMiddleUp, MouseMiddleClick, MouseMiddleDoubleClick},
{tcell.Button3, MouseRightDown, MouseRightUp, MouseRightClick, MouseRightDoubleClick},
} {
if btnDiff&x.button != 0 {
if ebuttons&x.button != 0 {
mouseEv(x.down)
} else {
mouseEv(x.up)
if !clickMoved {
if a.lastClickTime.Add(DoubleClickInterval).Before(time.Now()) {
mouseEv(x.click)
a.lastClickTime = time.Now()
} else {
mouseEv(x.dclick)
a.lastClickTime = time.Time{} // reset
}
}
}
}
}
for _, x := range []struct {
button tcell.ButtonMask
action MouseAction
}{
{tcell.WheelUp, WheelUp}, {tcell.WheelDown, WheelDown},
{tcell.WheelLeft, WheelLeft}, {tcell.WheelRight, WheelRight}} {
if ebuttons&x.button != 0 {
mouseEv(x.action)
}
}
return consumed, isMouseDownAction
}
// Stop stops the application, causing Run() to return.
func (a *Application) Stop() {
a.Lock()
@ -542,3 +699,30 @@ func (a *Application) QueueEvent(event tcell.Event) *Application {
a.events <- event
return a
}
// MouseAction indicates one of the actions the mouse is logically doing.
type MouseAction int16
const (
MouseMove MouseAction = iota
MouseLeftDown
MouseLeftUp
MouseLeftClick
MouseLeftDoubleClick
MouseMiddleDown
MouseMiddleUp
MouseMiddleClick
MouseMiddleDoubleClick
MouseRightDown
MouseRightUp
MouseRightClick
MouseRightDoubleClick
WheelUp
WheelDown
WheelLeft
WheelRight
)
// DoubleClickInterval specifies the maximum time between clicks
// to register a double click rather than click.
var DoubleClickInterval = 500 * time.Millisecond

@ -59,6 +59,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(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse)
}
// NewBox returns a Box without a border.
@ -193,6 +198,52 @@ 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(MouseAction, *tcell.EventMouse, func(p Primitive)) (bool, Primitive)) func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if b.mouseCapture != nil {
action, event = b.mouseCapture(action, event)
}
if event != nil && mouseHandler != nil {
consumed, capture = mouseHandler(action, event, setFocus)
}
return
}
}
// MouseHandler returns nil.
func (b *Box) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
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(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse)) *Box {
b.mouseCapture = capture
return b
}
func (b *Box) InRect(atX, atY int) bool {
x, y, w, h := b.GetRect()
return atX >= x && atX < x+w &&
atY >= y && atY < y+h
}
// GetMouseCapture returns the function installed with SetMouseCapture() or nil
// if no such function has been installed.
func (b *Box) GetMouseCapture() func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse) {
return b.mouseCapture
}
// SetBackgroundColor sets the box's background color.
func (b *Box) SetBackgroundColor(color tcell.Color) *Box {
b.backgroundColor = color

@ -135,3 +135,19 @@ func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Prim
}
})
}
// MouseHandler returns the mouse handler for this primitive.
func (b *Button) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return b.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !b.InRect(event.Position()) {
return false, nil
}
// Process mouse event.
if action == MouseLeftClick {
if b.selected != nil {
b.selected()
}
}
return true, nil
})
}

@ -201,3 +201,20 @@ func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
}
})
}
// MouseHandler returns the mouse handler for this primitive.
func (c *Checkbox) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return c.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !c.InRect(event.Position()) {
return false, nil
}
// Process mouse event.
if action == MouseLeftClick {
c.checked = !c.checked
if c.changed != nil {
c.changed(c.checked)
}
}
return true, nil
})
}

@ -15,6 +15,7 @@ func main() {
AddItem("Quit", "Press to exit", 'q', func() {
app.Stop()
})
app.EnableMouse(true)
if err := app.SetRoot(list, true).Run(); err != nil {
panic(err)
}

@ -91,6 +91,8 @@ func main() {
return event
})
app.EnableMouse(true)
// Start the application.
if err := app.SetRoot(layout, true).Run(); err != nil {
panic(err)

@ -394,22 +394,6 @@ func (d *DropDown) Draw(screen tcell.Screen) {
// InputHandler returns the handler for this primitive.
func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
// A helper function which selects an item in the drop-down list based on
// the current prefix.
evalPrefix := func() {
if len(d.prefix) > 0 {
for index, option := range d.options {
if strings.HasPrefix(strings.ToLower(option.Text), d.prefix) {
d.list.SetCurrentItem(index)
return
}
}
// Prefix does not match any item. Remove last rune.
r := []rune(d.prefix)
d.prefix = string(r[:len(r)-1])
}
}
// Process key event.
switch key := event.Key(); key {
case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown:
@ -418,45 +402,10 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
// If the first key was a letter already, it becomes part of the prefix.
if r := event.Rune(); key == tcell.KeyRune && r != ' ' {
d.prefix += string(r)
evalPrefix()
d.evalPrefix()
}
// Hand control over to the list.
d.open = true
optionBefore := d.currentOption
d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) {
// An option was selected. Close the list again.
d.open = false
setFocus(d)
d.currentOption = index
// Trigger "selected" event.
if d.selected != nil {
d.selected(d.options[d.currentOption].Text, d.currentOption)
}
if d.options[d.currentOption].Selected != nil {
d.options[d.currentOption].Selected()
}
}).SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyRune {
d.prefix += string(event.Rune())
evalPrefix()
} else if event.Key() == tcell.KeyBackspace || event.Key() == tcell.KeyBackspace2 {
if len(d.prefix) > 0 {
r := []rune(d.prefix)
d.prefix = string(r[:len(r)-1])
}
evalPrefix()
} else if event.Key() == tcell.KeyEscape {
d.open = false
d.currentOption = optionBefore
setFocus(d)
} else {
d.prefix = ""
}
return event
})
setFocus(d.list)
d.openList(setFocus)
case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab:
if d.done != nil {
d.done(key)
@ -468,6 +417,66 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
})
}
// A helper function which selects an item in the drop-down list based on
// the current prefix.
func (d *DropDown) evalPrefix() {
if len(d.prefix) > 0 {
for index, option := range d.options {
if strings.HasPrefix(strings.ToLower(option.Text), d.prefix) {
d.list.SetCurrentItem(index)
return
}
}
// Prefix does not match any item. Remove last rune.
r := []rune(d.prefix)
d.prefix = string(r[:len(r)-1])
}
}
// Hand control over to the list.
func (d *DropDown) openList(setFocus func(Primitive)) {
d.open = true
optionBefore := d.currentOption
d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) {
// An option was selected. Close the list again.
d.currentOption = index
d.closeList(setFocus)
// Trigger "selected" event.
if d.selected != nil {
d.selected(d.options[d.currentOption].Text, d.currentOption)
}
if d.options[d.currentOption].Selected != nil {
d.options[d.currentOption].Selected()
}
}).SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyRune {
d.prefix += string(event.Rune())
d.evalPrefix()
} else if event.Key() == tcell.KeyBackspace || event.Key() == tcell.KeyBackspace2 {
if len(d.prefix) > 0 {
r := []rune(d.prefix)
d.prefix = string(r[:len(r)-1])
}
d.evalPrefix()
} else if event.Key() == tcell.KeyEscape {
d.currentOption = optionBefore
d.closeList(setFocus)
} else {
d.prefix = ""
}
return event
})
setFocus(d.list)
}
func (d *DropDown) closeList(setFocus func(Primitive)) {
d.open = false
if d.list.HasFocus() {
setFocus(d)
}
}
// Focus is called by the application when the primitive receives focus.
func (d *DropDown) Focus(delegate func(p Primitive)) {
d.Box.Focus(delegate)
@ -483,3 +492,40 @@ func (d *DropDown) HasFocus() bool {
}
return d.hasFocus
}
func (d *DropDown) listClick(event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool) {
if !d.list.InRect(event.Position()) {
return false
}
// Mouse is within the list.
if handler := d.list.MouseHandler(); handler != nil {
consumed, _ := handler(MouseLeftClick, event, setFocus)
return consumed
}
return false
}
// MouseHandler returns the mouse handler for this primitive.
func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return d.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
inRect := d.InRect(event.Position()) // mouse in the dropdown box itself, not the list.
if !d.open && !inRect {
return false, nil
}
// Process mouse event.
if action == MouseLeftClick {
if !d.open { // not open
d.openList(setFocus)
} else { // if d.open
if !inRect {
d.listClick(event, setFocus)
}
d.closeList(setFocus)
}
}
if d.open {
return true, d // capture
}
return inRect, nil
})
}

@ -201,3 +201,20 @@ func (f *Flex) HasFocus() bool {
}
return false
}
// MouseHandler returns the mouse handler for this primitive.
func (f *Flex) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !f.InRect(event.Position()) {
return false, nil
}
// Process mouse event.
for _, item := range f.items {
consumed, capture = item.Item.MouseHandler()(action, event, setFocus)
if consumed {
return consumed, capture
}
}
return true, nil
})
}

@ -619,3 +619,26 @@ func (f *Form) focusIndex() int {
}
return -1
}
// MouseHandler returns the mouse handler for this primitive.
func (f *Form) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !f.InRect(event.Position()) {
return false, nil
}
// Process mouse event.
for _, item := range f.items {
consumed, capture = item.MouseHandler()(action, event, setFocus)
if consumed {
return consumed, capture
}
}
for _, button := range f.buttons {
consumed, capture = button.MouseHandler()(action, event, setFocus)
if consumed {
return consumed, capture
}
}
return true, nil
})
}

@ -155,3 +155,18 @@ func (f *Frame) HasFocus() bool {
}
return false
}
// MouseHandler returns the mouse handler for this primitive.
func (f *Frame) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !f.InRect(event.Position()) {
return false, nil
}
// Process mouse event.
consumed, capture = f.primitive.MouseHandler()(action, event, setFocus)
if consumed {
return consumed, capture
}
return true, nil
})
}

@ -660,3 +660,20 @@ func (g *Grid) Draw(screen tcell.Screen) {
}
}
}
// MouseHandler returns the mouse handler for this primitive.
func (g *Grid) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return g.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !g.InRect(event.Position()) {
return false, nil
}
// Process mouse event.
for _, item := range g.items {
consumed, capture = item.Item.MouseHandler()(action, event, setFocus)
if consumed {
return consumed, capture
}
}
return true, nil
})
}

@ -591,3 +591,17 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
}
})
}
// MouseHandler returns the mouse handler for this primitive.
func (i *InputField) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return i.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !i.InRect(event.Position()) {
return false, nil
}
// Process mouse event.
if action == MouseLeftDown {
setFocus(i)
}
return true, nil
})
}

@ -558,3 +558,49 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
}
})
}
// returns -1 if not found.
func (l *List) indexAtPoint(atX, atY int) int {
_, y, _, h := l.GetInnerRect()
if atY < y || atY >= y+h {
return -1
}
n := atY - y
if l.showSecondaryText {
n /= 2
}
if n >= len(l.items) {
return -1
}
return n
}
// MouseHandler returns the mouse handler for this primitive.
func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return l.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !l.InRect(event.Position()) {
return false, nil
}
// Process mouse event.
if action == MouseLeftClick {
atX, atY := event.Position()
index := l.indexAtPoint(atX, atY)
if index != -1 {
item := l.items[index]
if item.Selected != nil {
item.Selected()
}
if l.selected != nil {
l.selected(index, item.MainText, item.SecondaryText, item.Shortcut)
}
if index != l.currentItem && l.changed != nil {
l.changed(index, item.MainText, item.SecondaryText, item.Shortcut)
}
l.currentItem = index
}
}
return true, nil
})
}

@ -175,3 +175,18 @@ func (m *Modal) Draw(screen tcell.Screen) {
m.frame.SetRect(x, y, width, height)
m.frame.Draw(screen)
}
// MouseHandler returns the mouse handler for this primitive.
func (m *Modal) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return m.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !m.InRect(event.Position()) {
return false, nil
}
// Process mouse event.
consumed, capture = m.frame.MouseHandler()(action, event, setFocus)
if consumed {
return consumed, capture
}
return true, nil
})
}

@ -278,3 +278,22 @@ func (p *Pages) Draw(screen tcell.Screen) {
page.Item.Draw(screen)
}
}
// MouseHandler returns the mouse handler for this primitive.
func (p *Pages) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return p.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !p.InRect(event.Position()) {
return false, nil
}
// Process mouse event.
for _, page := range p.pages {
if page.Visible {
consumed, capture = page.Item.MouseHandler()(action, event, setFocus)
if consumed {
return consumed, capture
}
}
}
return true, nil
})
}

@ -43,4 +43,14 @@ type Primitive interface {
// GetFocusable returns the item's Focusable.
GetFocusable() Focusable
// MouseHandler returns a handler which receives mouse events.
// It is called by the Application class.
//
// A value of nil 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(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive)
}

Loading…
Cancel
Save