Merge pull request #422 from rivo/mouse

Add mouse support
This commit is contained in:
rivo 2020-03-29 21:43:46 +02:00 committed by Trevor Slocum
parent aae1af2a19
commit 1f765c8695
36 changed files with 858 additions and 543 deletions

View file

@ -8,11 +8,52 @@ import (
"github.com/gdamore/tcell"
)
// The size of the event/update/redraw channels.
const queueSize = 100
const (
// The size of the event/update/redraw channels.
queueSize = 100
// The minimum duration between resize event callbacks.
const resizeEventThrottle = 200 * time.Millisecond
// The minimum time between two consecutive redraws.
redrawPause = 50 * time.Millisecond
// The minimum duration between resize event callbacks.
resizeEventThrottle = 200 * time.Millisecond
)
// DoubleClickInterval specifies the maximum time between clicks to register a
// double click rather than click.
var DoubleClickInterval = 500 * time.Millisecond
// MouseAction indicates one of the actions the mouse is logically doing.
type MouseAction int16
// Available mouse actions.
const (
MouseMove MouseAction = iota
MouseLeftDown
MouseLeftUp
MouseLeftClick
MouseLeftDoubleClick
MouseMiddleDown
MouseMiddleUp
MouseMiddleClick
MouseMiddleDoubleClick
MouseRightDown
MouseRightUp
MouseRightClick
MouseRightDoubleClick
MouseScrollUp
MouseScrollDown
MouseScrollLeft
MouseScrollRight
)
// queuedUpdate represented the execution of f queued by
// Application.QueueUpdate(). The "done" channel receives exactly one element
// after f has executed.
type queuedUpdate struct {
f func()
done chan struct{}
}
// Application represents the top node of an application.
//
@ -84,14 +125,13 @@ type Application struct {
// 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
mouseCapture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction)
// A temporary capture function overriding the above.
tempMouseCapture func(event *EventMouse) *EventMouse
lastMouseX, lastMouseY int
lastMouseBtn tcell.ButtonMask
lastMouseTarget Primitive // nil if none
mouseCapturingPrimitive Primitive // A Primitive returned by a MouseHandler which will capture future mouse events.
lastMouseX, lastMouseY int // The last position of the mouse.
mouseDownX, mouseDownY int // The position of the mouse when its button was last pressed.
lastMouseClick time.Time // The time when a mouse button was last clicked.
lastMouseButtons tcell.ButtonMask // The last mouse button state.
sync.RWMutex
}
@ -131,45 +171,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
// SetMouseCapture sets a function which captures mouse events (consisting of
// the original tcell mouse event and the semantic mouse action) 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.Lock()
// the event processing by returning a nil mouse event.
func (a *Application) SetMouseCapture(capture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction)) *Application {
a.mouseCapture = capture
a.Unlock()
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 {
a.RLock()
defer a.RUnlock()
func (a *Application) GetMouseCapture() func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction) {
return a.mouseCapture
}
// SetTemporaryMouseCapture temporarily overrides the normal capture function.
// Calling this function from anywhere other than a widget may result in
// unexpected behavior.
func (a *Application) SetTemporaryMouseCapture(capture func(event *EventMouse) *EventMouse) *Application {
a.Lock()
a.tempMouseCapture = capture
a.Unlock()
return a
}
// GetTemporaryMouseCapture returns the function installed with
// SetTemporaryMouseCapture() or nil if no such function has been installed.
func (a *Application) GetTemporaryMouseCapture() func(event *EventMouse) *EventMouse {
a.RLock()
defer a.RUnlock()
return a.tempMouseCapture
}
// 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.
@ -199,10 +216,17 @@ func (a *Application) SetScreen(screen tcell.Screen) *Application {
}
// EnableMouse enables mouse events.
func (a *Application) EnableMouse() *Application {
func (a *Application) EnableMouse(enable bool) *Application {
a.Lock()
a.enableMouse = true
a.Unlock()
defer a.Unlock()
if enable != a.enableMouse && a.screen != nil {
if enable {
a.screen.EnableMouse()
} else {
a.screen.DisableMouse()
}
}
a.enableMouse = enable
return a
}
@ -301,10 +325,7 @@ EventLoop:
a.RLock()
p := a.focus
inputCapture := a.inputCapture
mouseCapture := a.mouseCapture
tempMouseCapture := a.tempMouseCapture
screen := a.screen
root := a.root
a.RUnlock()
switch event := event.(type) {
@ -368,85 +389,14 @@ EventLoop:
a.draw()
case *tcell.EventMouse:
atX, atY := event.Position()
btn := event.Buttons()
pstack := a.appendStackAtPoint(nil, atX, atY)
var punderMouse Primitive
if len(pstack) > 0 {
punderMouse = pstack[len(pstack)-1]
}
var ptarget Primitive
if a.lastMouseBtn != 0 {
// While a button is down, the same primitive gets events.
ptarget = a.lastMouseTarget
}
if ptarget == nil {
ptarget = punderMouse
if ptarget == nil {
ptarget = root // Fallback to root.
}
}
a.lastMouseTarget = ptarget
// Calculate mouse actions.
var act MouseAction
if atX != a.lastMouseX || atY != a.lastMouseY {
act |= MouseMove
a.lastMouseX = atX
a.lastMouseY = atY
}
btnDiff := btn ^ a.lastMouseBtn
if btnDiff != 0 {
if btn&btnDiff != 0 {
act |= MouseDown
}
if a.lastMouseBtn&btnDiff != 0 {
act |= MouseUp
}
if a.lastMouseBtn == tcell.Button1 && btn == 0 {
if ptarget == punderMouse {
// Only if Button1 and mouse up over same p.
act |= MouseClick
}
}
a.lastMouseBtn = btn
}
event2 := NewEventMouse(event, ptarget, a, act)
// Intercept event.
if tempMouseCapture != nil {
event2 = tempMouseCapture(event2)
if event2 == nil {
a.draw()
continue // Don't forward event.
}
}
if mouseCapture != nil {
event2 = mouseCapture(event2)
if event2 == nil {
a.draw()
continue // Don't forward event.
}
}
if ptarget == punderMouse {
// Observe mouse events inward ("capture")
for _, pp := range pstack {
// If the primitive has this ObserveMouseEvent func.
if pp, ok := pp.(interface {
ObserveMouseEvent(*EventMouse)
}); ok {
pp.ObserveMouseEvent(event2)
}
}
}
if handler := ptarget.MouseHandler(); handler != nil {
handler(event2)
consumed, isMouseDownAction := a.fireMouseActions(event)
if consumed {
a.draw()
}
a.lastMouseButtons = event.Buttons()
if isMouseDownAction {
a.mouseDownX, a.mouseDownY = event.Position()
}
}
// If we have updates, now is the time to execute them.
@ -462,48 +412,103 @@ EventLoop:
return nil
}
func findAtPoint(atX, atY int, p Primitive, capture func(p Primitive)) Primitive {
x, y, w, h := p.GetRect()
if atX < x || atY < y {
return nil
// fireMouseActions analyzes the provided mouse event, derives mouse actions
// from it and then forwards them to the corresponding primitives.
func (a *Application) fireMouseActions(event *tcell.EventMouse) (consumed, isMouseDownAction bool) {
// We want to relay follow-up events to the same target primitive.
var targetPrimitive Primitive
// Helper function to fire a mouse action.
fire := func(action MouseAction) {
switch action {
case MouseLeftDown, MouseMiddleDown, MouseRightDown:
isMouseDownAction = true
}
// Intercept event.
if a.mouseCapture != nil {
event, action = a.mouseCapture(event, action)
if event == nil {
consumed = true
return // Don't forward event.
}
}
// Determine the target primitive.
var primitive, capturingPrimitive Primitive
if a.mouseCapturingPrimitive != nil {
primitive = a.mouseCapturingPrimitive
targetPrimitive = a.mouseCapturingPrimitive
} else if targetPrimitive != nil {
primitive = targetPrimitive
} else {
primitive = a.root
}
if primitive != nil {
if handler := primitive.MouseHandler(); handler != nil {
var wasConsumed bool
wasConsumed, capturingPrimitive = handler(action, event, func(p Primitive) {
a.SetFocus(p)
})
if wasConsumed {
consumed = true
}
}
}
a.mouseCapturingPrimitive = capturingPrimitive
}
if atX >= x+w || atY >= y+h {
return nil
x, y := event.Position()
buttons := event.Buttons()
clickMoved := x != a.mouseDownX || y != a.mouseDownY
buttonChanges := buttons ^ a.lastMouseButtons
if x != a.lastMouseX || y != a.lastMouseY {
fire(MouseMove)
a.lastMouseX = x
a.lastMouseY = y
}
if capture != nil {
capture(p)
}
bestp := p
for _, pchild := range p.GetChildren() {
x := findAtPoint(atX, atY, pchild, capture)
if x != nil {
// Always overwrite if we find another one,
// this is because if any overlap, the last one is "on top".
bestp = x
for _, buttonEvent := 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 buttonChanges&buttonEvent.button != 0 {
if buttons&buttonEvent.button != 0 {
fire(buttonEvent.down)
} else {
fire(buttonEvent.up)
if !clickMoved {
if a.lastMouseClick.Add(DoubleClickInterval).Before(time.Now()) {
fire(buttonEvent.click)
a.lastMouseClick = time.Now()
} else {
fire(buttonEvent.dclick)
a.lastMouseClick = time.Time{} // reset
}
}
}
}
}
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()
for _, wheelEvent := range []struct {
button tcell.ButtonMask
action MouseAction
}{
{tcell.WheelUp, MouseScrollUp},
{tcell.WheelDown, MouseScrollDown},
{tcell.WheelLeft, MouseScrollLeft},
{tcell.WheelRight, MouseScrollRight}} {
if buttons&wheelEvent.button != 0 {
fire(wheelEvent.action)
}
}
return findAtPoint(atX, atY, a.root, nil)
}
// The last element appended to buf is the primitive clicked,
// the preceeding are its parents.
func (a *Application) appendStackAtPoint(buf []Primitive, atX, atY int) []Primitive {
a.RLock()
defer a.RUnlock()
findAtPoint(atX, atY, a.root, func(p Primitive) {
buf = append(buf, p)
})
return buf
return consumed, isMouseDownAction
}
// Stop stops the application, causing Run() to return.

60
box.go
View file

@ -59,9 +59,9 @@ type Box struct {
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
// event to be forwarded to the primitive's default mouse event handler (at
// least one nil if nothing should be forwarded).
mouseCapture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse)
l sync.RWMutex
}
@ -229,50 +229,56 @@ func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey {
}
// 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.
// functionality to capture mouse events (see SetMouseCapture()) before passing
// them 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) {
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 {
event = b.mouseCapture(event)
action, event = b.mouseCapture(action, event)
}
if event != nil && mouseHandler != nil {
mouseHandler(event)
consumed, capture = mouseHandler(action, event, setFocus)
}
return
}
}
// MouseHandler returns nil.
func (b *Box) MouseHandler() func(event *EventMouse) {
b.l.RLock()
defer b.l.RUnlock()
return b.WrapMouseHandler(nil)
func (b *Box) 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 action == MouseLeftClick && b.InRect(event.Position()) {
setFocus(b)
consumed = true
}
return
})
}
// 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.
// SetMouseCapture sets a function which captures mouse events (consisting of
// the original tcell mouse event and the semantic mouse action) before they are
// forwarded to the primitive's default mouse event handler. This function can
// then choose to forward that event (or a different one) by returning it or
// returning a nil mouse event, in which case 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.l.Lock()
defer b.l.Unlock()
func (b *Box) SetMouseCapture(capture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse)) *Box {
b.mouseCapture = capture
return b
}
// InRect returns true if the given coordinate is within the bounds of the box's
// rectangle.
func (b *Box) InRect(x, y int) bool {
rectX, rectY, width, height := b.GetRect()
return x >= rectX && x < rectX+width && y >= rectY && y < rectY+height
}
// GetMouseCapture returns the function installed with SetMouseCapture() or nil
// if no such function has been installed.
func (b *Box) GetMouseCapture() func(*EventMouse) *EventMouse {
b.l.RLock()
defer b.l.RUnlock()
func (b *Box) GetMouseCapture() func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse) {
return b.mouseCapture
}

View file

@ -167,13 +167,21 @@ 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(event *EventMouse) {
return b.WrapMouseHandler(func(event *EventMouse) {
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 event.Action()&MouseClick != 0 {
if action == MouseLeftClick {
setFocus(b)
if b.selected != nil {
b.selected()
}
consumed = true
}
return
})
}

View file

@ -279,16 +279,24 @@ 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(event *EventMouse) {
return c.WrapMouseHandler(func(event *EventMouse) {
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) {
x, y := event.Position()
_, rectY, _, _ := c.GetInnerRect()
if !c.InRect(x, y) {
return false, nil
}
// Process mouse event.
if event.Action()&MouseClick != 0 {
c.Lock()
if action == MouseLeftClick && y == rectY {
setFocus(c)
c.checked = !c.checked
c.Unlock()
if c.changed != nil {
c.changed(c.checked)
}
consumed = true
}
return
})
}

View file

@ -9,7 +9,7 @@ func main() {
app.Stop()
})
button.SetBorder(true).SetRect(0, 0, 22, 3)
if err := app.SetRoot(button, false).Run(); err != nil {
if err := app.SetRoot(button, false).EnableMouse(true).Run(); err != nil {
panic(err)
}
}

View file

@ -1,12 +1,14 @@
// Demo code for the Checkbox primitive.
package main
import "gitlab.com/tslocum/cview"
import (
"gitlab.com/tslocum/cview"
)
func main() {
app := cview.NewApplication()
checkbox := cview.NewCheckbox().SetLabel("Hit Enter to check box: ")
if err := app.SetRoot(checkbox, true).Run(); err != nil {
if err := app.SetRoot(checkbox, true).EnableMouse(true).Run(); err != nil {
panic(err)
}
}

View file

@ -8,7 +8,7 @@ func main() {
dropdown := cview.NewDropDown().
SetLabel("Select an option (hit Enter): ").
SetOptions([]string{"First", "Second", "Third", "Fourth", "Fifth"}, nil)
if err := app.SetRoot(dropdown, true).Run(); err != nil {
if err := app.SetRoot(dropdown, true).EnableMouse(true).Run(); err != nil {
panic(err)
}
}

View file

@ -14,7 +14,7 @@ func main() {
AddItem(cview.NewBox().SetBorder(true).SetTitle("Middle (3 x height of Top)"), 0, 3, false).
AddItem(cview.NewBox().SetBorder(true).SetTitle("Bottom (5 rows)"), 5, 1, false), 0, 2, false).
AddItem(cview.NewBox().SetBorder(true).SetTitle("Right (20 cols)"), 20, 1, false)
if err := app.SetRoot(flex, true).Run(); err != nil {
if err := app.SetRoot(flex, true).EnableMouse(true).Run(); err != nil {
panic(err)
}
}

View file

@ -18,7 +18,7 @@ func main() {
app.Stop()
})
form.SetBorder(true).SetTitle("Enter some data").SetTitleAlign(cview.AlignLeft)
if err := app.SetRoot(form, true).Run(); err != nil {
if err := app.SetRoot(form, true).EnableMouse(true).Run(); err != nil {
panic(err)
}
}

View file

@ -16,7 +16,7 @@ func main() {
AddText("Header second middle", true, cview.AlignCenter, tcell.ColorRed).
AddText("Footer middle", false, cview.AlignCenter, tcell.ColorGreen).
AddText("Footer second middle", false, cview.AlignCenter, tcell.ColorGreen)
if err := app.SetRoot(frame, true).Run(); err != nil {
if err := app.SetRoot(frame, true).EnableMouse(true).Run(); err != nil {
panic(err)
}
}

View file

@ -32,7 +32,7 @@ func main() {
AddItem(main, 1, 1, 1, 1, 0, 100, false).
AddItem(sideBar, 1, 2, 1, 1, 0, 100, false)
if err := cview.NewApplication().SetRoot(grid, true).Run(); err != nil {
if err := cview.NewApplication().SetRoot(grid, true).EnableMouse(true).Run(); err != nil {
panic(err)
}
}

View file

@ -16,7 +16,7 @@ func main() {
SetDoneFunc(func(key tcell.Key) {
app.Stop()
})
if err := app.SetRoot(inputField, true).Run(); err != nil {
if err := app.SetRoot(inputField, true).EnableMouse(true).Run(); err != nil {
panic(err)
}
}

View file

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

View file

@ -15,7 +15,7 @@ func main() {
app.Stop()
}
})
if err := app.SetRoot(modal, false).Run(); err != nil {
if err := app.SetRoot(modal, false).EnableMouse(true).Run(); err != nil {
panic(err)
}
}

View file

@ -29,7 +29,7 @@ func main() {
page == 0)
}(page)
}
if err := app.SetRoot(pages, true).Run(); err != nil {
if err := app.SetRoot(pages, true).EnableMouse(true).Run(); err != nil {
panic(err)
}
}

View file

@ -19,6 +19,7 @@ const logo = `
const (
subtitle = `Terminal-based user interface toolkit`
navigation = `Ctrl-N: Next slide Ctrl-P: Previous slide Ctrl-C: Exit`
mouse = `(or use your mouse)`
)
// Cover returns the cover page.
@ -44,7 +45,8 @@ func Cover(nextSlide func()) (title string, content cview.Primitive) {
SetBorders(0, 0, 0, 0, 0, 0).
AddText(subtitle, true, cview.AlignCenter, tcell.ColorWhite).
AddText("", true, cview.AlignCenter, tcell.ColorWhite).
AddText(navigation, true, cview.AlignCenter, tcell.ColorDarkMagenta)
AddText(navigation, true, cview.AlignCenter, tcell.ColorDarkMagenta).
AddText(mouse, true, cview.AlignCenter, tcell.ColorDarkMagenta)
// Create a Flex layout that centers the logo and subtitle.
flex := cview.NewFlex().

View file

@ -62,27 +62,29 @@ func main() {
End,
}
pages := cview.NewPages()
// The bottom row has some info on where we are.
info := cview.NewTextView().
SetDynamicColors(true).
SetRegions(true).
SetWrap(false)
SetWrap(false).
SetHighlightedFunc(func(added, removed, remaining []string) {
pages.SwitchToPage(added[0])
})
// Create the pages for all slides.
currentSlide := 0
info.Highlight(strconv.Itoa(currentSlide))
pages := cview.NewPages()
previousSlide := func() {
currentSlide = (currentSlide - 1 + len(slides)) % len(slides)
info.Highlight(strconv.Itoa(currentSlide)).
slide, _ := strconv.Atoi(info.GetHighlights()[0])
slide = (slide - 1 + len(slides)) % len(slides)
info.Highlight(strconv.Itoa(slide)).
ScrollToHighlight()
pages.SwitchToPage(strconv.Itoa(currentSlide))
}
nextSlide := func() {
currentSlide = (currentSlide + 1) % len(slides)
info.Highlight(strconv.Itoa(currentSlide)).
slide, _ := strconv.Atoi(info.GetHighlights()[0])
slide = (slide + 1) % len(slides)
info.Highlight(strconv.Itoa(slide)).
ScrollToHighlight()
pages.SwitchToPage(strconv.Itoa(currentSlide))
}
cursor := 0
@ -91,11 +93,12 @@ func main() {
slideRegions = append(slideRegions, cursor)
title, primitive := slide(nextSlide)
pages.AddPage(strconv.Itoa(index), primitive, true, index == currentSlide)
pages.AddPage(strconv.Itoa(index), primitive, true, index == 0)
fmt.Fprintf(info, `%d ["%d"][darkcyan]%s[white][""] `, index+1, index, title)
cursor += len(title) + 4
}
info.Highlight("0")
// Create the main layout.
layout := cview.NewFlex().
@ -113,37 +116,8 @@ func main() {
return event
})
app.EnableMouse()
var screenHeight int
app.SetAfterResizeFunc(func(_ int, height int) {
screenHeight = height
})
app.SetMouseCapture(func(event *cview.EventMouse) *cview.EventMouse {
atX, atY := event.Position()
if event.Action()&cview.MouseDown != 0 && atY == screenHeight-1 {
slideClicked := -1
for i, region := range slideRegions {
if atX >= region {
slideClicked = i
}
}
if slideClicked >= 0 {
currentSlide = slideClicked
info.Highlight(strconv.Itoa(currentSlide)).
ScrollToHighlight()
pages.SwitchToPage(strconv.Itoa(currentSlide))
}
return nil
}
return event
})
// Start the application.
if err := app.SetRoot(layout, true).Run(); err != nil {
if err := app.SetRoot(layout, true).EnableMouse(true).Run(); err != nil {
panic(err)
}
}

View file

@ -39,7 +39,7 @@ func main() {
table.GetCell(row, column).SetTextColor(tcell.ColorRed)
table.SetSelectable(false, false)
})
if err := app.SetRoot(table, true).Run(); err != nil {
if err := app.SetRoot(table, true).EnableMouse(true).Run(); err != nil {
panic(err)
}
}

View file

@ -63,7 +63,7 @@ func main() {
}
})
textView.SetBorder(true)
if err := app.SetRoot(textView, true).Run(); err != nil {
if err := app.SetRoot(textView, true).EnableMouse(true).Run(); err != nil {
panic(err)
}
}

View file

@ -56,7 +56,7 @@ func main() {
}
})
if err := cview.NewApplication().SetRoot(tree, true).Run(); err != nil {
if err := cview.NewApplication().SetRoot(tree, true).EnableMouse(true).Run(); err != nil {
panic(err)
}
}

4
doc.go
View file

@ -59,10 +59,6 @@ Application.EnableMouse documentation.
Mouse events are passed to:
- The handler set with SetTemporaryMouseCapture, which is reserved for use by
widgets to temporarily intercept mouse events, such as to close a Dropdown when
the user clicks outside of the list.
- The handler set with SetMouseCapture, which is reserved for use by application
developers to permanently intercept mouse events.

View file

@ -61,27 +61,33 @@ func ExampleNewApplication() {
// Example of an application with mouse support.
func ExampleApplication_EnableMouse() {
// Initialize application and enable mouse support.
app := NewApplication().EnableMouse()
app := NewApplication()
// Create a textview.
tv := NewTextView().SetText("Click somewhere!")
// Set a mouse capture function which prints where the mouse was clicked.
app.SetMouseCapture(func(event *EventMouse) *EventMouse {
if event.Action()&MouseDown != 0 && event.Buttons()&tcell.Button1 != 0 {
app.SetMouseCapture(func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction) {
if action == MouseLeftClick || action == MouseLeftDoubleClick {
actionLabel := "click"
if action == MouseLeftDoubleClick {
actionLabel = "double-click"
}
x, y := event.Position()
fmt.Fprintf(tv, "\nYou clicked at %d,%d! Amazing!", x, y)
fmt.Fprintf(tv, "\nYou %sed at %d,%d! Amazing!", actionLabel, x, y)
// Return nil to stop propagating the event to any remaining handlers.
return nil
return nil, 0
}
// Return the event to continue propagating it.
return event
return event, action
})
// Run the application.
if err := app.SetRoot(tv, true).Run(); err != nil {
if err := app.EnableMouse(true).SetRoot(tv, true).Run(); err != nil {
panic(err)
}
}

View file

@ -81,6 +81,9 @@ type DropDown struct {
// selection.
selected func(text string, index int)
// Set to true when mouse dragging is in progress.
dragging bool
sync.RWMutex
}
@ -482,7 +485,7 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
d.evalPrefix()
}
d.openList(setFocus, nil)
d.openList(setFocus)
case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab:
if d.done != nil {
d.done(key)
@ -494,8 +497,7 @@ 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.
// evalPrefix 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 {
@ -504,31 +506,33 @@ func (d *DropDown) evalPrefix() {
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), app *Application) {
// openList hands control over to the embedded List primitive.
func (d *DropDown) openList(setFocus func(Primitive)) {
d.open = true
optionBefore := d.currentOption
d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) {
if d.dragging {
return // If we're dragging the mouse, we don't want to trigger any events.
}
// An option was selected. Close the list again.
d.currentOption = index
d.closeList(setFocus, app)
d.closeList(setFocus)
// Trigger "selected" event.
if d.selected != nil {
d.Unlock()
d.selected(d.options[d.currentOption].Text, d.currentOption)
d.Lock()
}
if d.options[d.currentOption].Selected != nil {
d.Unlock()
d.options[d.currentOption].Selected()
d.Lock()
}
}).SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyRune {
@ -542,50 +546,20 @@ func (d *DropDown) openList(setFocus func(Primitive), app *Application) {
d.evalPrefix()
} else if event.Key() == tcell.KeyEscape {
d.currentOption = optionBefore
d.closeList(setFocus, app)
d.closeList(setFocus)
} else {
d.prefix = ""
}
return event
})
if app != nil {
app.SetTemporaryMouseCapture(func(event *EventMouse) *EventMouse {
if d.open {
// Forward the mouse event to the list.
atX, atY := event.Position()
x, y, w, h := d.list.GetInnerRect()
if atX >= x && atY >= y && atX < x+w && atY < y+h {
// Mouse is within the list.
if handler := d.list.MouseHandler(); handler != nil {
if event.Action()&MouseUp != 0 {
// Treat mouse up as click here.
// This allows you to expand and select in one go.
event = NewEventMouse(event.EventMouse,
event.Target(), event.Application(),
event.Action()|MouseClick)
}
handler(event)
return nil // handled
}
} else {
// Mouse not within the list.
if event.Action()&MouseDown != 0 {
// If a mouse button was pressed, cancel this capture.
d.closeList(event.SetFocus, app)
}
}
}
return event
})
}
setFocus(d.list)
}
func (d *DropDown) closeList(setFocus func(Primitive), app *Application) {
if app != nil {
app.SetTemporaryMouseCapture(nil)
}
// closeList closes the embedded List element by hiding it and removing focus
// from it.
func (d *DropDown) closeList(setFocus func(Primitive)) {
d.open = false
if d.list.HasFocus() {
setFocus(d)
@ -612,20 +586,43 @@ func (d *DropDown) HasFocus() bool {
}
// MouseHandler returns the mouse handler for this primitive.
func (d *DropDown) MouseHandler() func(event *EventMouse) {
return d.WrapMouseHandler(func(event *EventMouse) {
// Process mouse event.
if event.Action()&MouseDown != 0 && event.Buttons()&tcell.Button1 != 0 {
d.Lock()
defer d.Unlock()
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) {
// Was the mouse event in the drop-down box itself (or on its label)?
x, y := event.Position()
_, rectY, _, _ := d.GetInnerRect()
inRect := y == rectY
if !d.open && !inRect {
return d.InRect(x, y), nil // No, and it's not expanded either. Ignore.
}
//d.open = !d.open
//event.SetFocus(d)
if d.open {
d.closeList(event.SetFocus, event.Application())
} else {
d.openList(event.SetFocus, event.Application())
// Handle dragging. Clicks are implicitly handled by this logic.
switch action {
case MouseLeftDown:
consumed = d.open || inRect
capture = d
if !d.open {
d.openList(setFocus)
d.dragging = true
} else if consumed, _ := d.list.MouseHandler()(MouseLeftClick, event, setFocus); !consumed {
d.closeList(setFocus) // Close drop-down if clicked outside of it.
}
case MouseMove:
if d.dragging {
// We pretend it's a left click so we can see the selection during
// dragging. Because we don't act upon it, it's not a problem.
d.list.MouseHandler()(MouseLeftClick, event, setFocus)
consumed = true
capture = d
}
case MouseLeftUp:
if d.dragging {
d.dragging = false
d.list.MouseHandler()(MouseLeftClick, event, setFocus)
consumed = true
}
}
return
})
}

View file

@ -1,47 +0,0 @@
package cview
import "github.com/gdamore/tcell"
// MouseAction are bit flags indicating what the mouse is logically doing.
type MouseAction int
// All MouseActions
const (
MouseDown MouseAction = 1 << iota
MouseUp
MouseClick // Button1 only.
MouseMove // The mouse position changed.
)
// EventMouse is the mouse event info.
type EventMouse struct {
*tcell.EventMouse
target Primitive
app *Application
action MouseAction
}
// Target gets the target Primitive of the mouse event.
func (e *EventMouse) Target() Primitive {
return e.target
}
// Application gets the event originating *Application.
func (e *EventMouse) Application() *Application {
return e.app
}
// Action gets the mouse action of this event.
func (e *EventMouse) Action() MouseAction {
return e.action
}
// SetFocus will set focus to the primitive.
func (e *EventMouse) SetFocus(p Primitive) {
e.app.SetFocus(p)
}
// NewEventMouse creates a new mouse event.
func NewEventMouse(base *tcell.EventMouse, target Primitive, app *Application, action MouseAction) *EventMouse {
return &EventMouse{base, target, app, action}
}

25
flex.go
View file

@ -226,14 +226,21 @@ func (f *Flex) HasFocus() bool {
return false
}
// GetChildren returns all primitives that have been added.
func (f *Flex) GetChildren() []Primitive {
f.Lock()
defer f.Unlock()
// 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
}
children := make([]Primitive, len(f.items))
for i, item := range f.items {
children[i] = item.Item
}
return children
// Pass mouse events along to the first child item that takes it.
for _, item := range f.items {
consumed, capture = item.Item.MouseHandler()(action, event, setFocus)
if consumed {
return
}
}
return
})
}

48
form.go
View file

@ -754,20 +754,38 @@ func (f *Form) focusIndex() int {
return -1
}
// GetChildren returns all primitives that have been added.
func (f *Form) GetChildren() []Primitive {
f.Lock()
defer f.Unlock()
// 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
}
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
// Determine items to pass mouse events to.
for _, item := range f.items {
consumed, capture = item.MouseHandler()(action, event, setFocus)
if consumed {
return
}
}
for _, button := range f.buttons {
consumed, capture = button.MouseHandler()(action, event, setFocus)
if consumed {
return
}
}
// A mouse click anywhere else will return the focus to the last selected
// element.
if action == MouseLeftClick {
if f.focusedElement < len(f.items) {
setFocus(f.items[f.focusedElement])
} else if f.focusedElement < len(f.items)+len(f.buttons) {
setFocus(f.buttons[f.focusedElement-len(f.items)])
}
consumed = true
}
return
})
}

View file

@ -14,8 +14,8 @@ type frameText struct {
Color tcell.Color // The text color.
}
// Frame is a wrapper which adds a border around another primitive. The top area
// (header) and the bottom area (footer) may also contain text.
// Frame is a wrapper which adds space around another primitive. In addition,
// the top area (header) and the bottom area (footer) may also contain text.
//
// See https://gitlab.com/tslocum/cview/wiki/Frame for an example.
type Frame struct {
@ -179,10 +179,14 @@ func (f *Frame) HasFocus() bool {
return false
}
// GetChildren returns all primitives that have been added.
func (f *Frame) GetChildren() []Primitive {
f.Lock()
defer f.Unlock()
// 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
}
return []Primitive{f.primitive}
// Pass mouse events on to contained primitive.
return f.primitive.MouseHandler()(action, event, setFocus)
})
}

25
grid.go
View file

@ -720,14 +720,21 @@ func (g *Grid) Draw(screen tcell.Screen) {
}
}
// GetChildren returns all primitives that have been added.
func (g *Grid) GetChildren() []Primitive {
g.Lock()
defer g.Unlock()
// 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
}
children := make([]Primitive, len(g.items))
for i, item := range g.items {
children[i] = item.Item
}
return children
// Pass mouse events along to the first child item that takes it.
for _, item := range g.items {
consumed, capture = item.Item.MouseHandler()(action, event, setFocus)
if consumed {
return
}
}
return
})
}

View file

@ -69,9 +69,6 @@ type InputField struct {
// The cursor position as a byte index into the text string.
cursorPos int
// The number of bytes of the text string skipped ahead while drawing.
offset int
// An optional autocomplete function which receives the current text of the
// input field and returns a slice of strings to be displayed in a drop-down
// selection.
@ -96,6 +93,12 @@ type InputField struct {
// this form item.
finished func(tcell.Key)
// The x-coordinate of the input field as determined during the last call to Draw().
fieldX int
// The number of bytes of the text string skipped ahead while drawing.
offset int
sync.RWMutex
}
@ -396,6 +399,7 @@ func (i *InputField) Draw(screen tcell.Screen) {
}
// Draw input area.
i.fieldX = x
fieldWidth := i.fieldWidth
if fieldWidth == 0 {
fieldWidth = math.MaxInt32
@ -681,11 +685,32 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
}
// MouseHandler returns the mouse handler for this primitive.
func (i *InputField) MouseHandler() func(event *EventMouse) {
return i.WrapMouseHandler(func(event *EventMouse) {
// Process mouse event.
if event