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.Action()&MouseDown != 0 {
event.SetFocus(i)
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) {
x, y := event.Position()
_, rectY, _, _ := i.GetInnerRect()
if !i.InRect(x, y) {
return false, nil
}
// Process mouse event.
if action == MouseLeftClick && y == rectY {
// Determine where to place the cursor.
if x >= i.fieldX {
if !iterateString(i.text, func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth int) bool {
if x-i.fieldX < screenPos+screenWidth {
i.cursorPos = textPos
return true
}
return false
}) {
i.cursorPos = len(i.text)
}
}
setFocus(i)
consumed = true
}
return
})
}

60
list.go
View File

@ -696,54 +696,68 @@ 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 {
// indexAtPoint returns the index of the list item found at the given position
// or a negative value if there is no such list item.
func (l *List) indexAtPoint(x, y int) int {
rectX, rectY, width, height := l.GetInnerRect()
if rectX < 0 || rectX >= rectX+width || y < rectY || y >= rectY+height {
return -1
}
n := atY - y
index := y - rectY
if l.showSecondaryText {
n /= 2
index /= 2
}
index += l.offset
if n >= len(l.items) {
if index >= len(l.items) {
return -1
}
return n
return index
}
// MouseHandler returns the mouse handler for this primitive.
func (l *List) MouseHandler() func(event *EventMouse) {
return l.WrapMouseHandler(func(event *EventMouse) {
// Process mouse event.
if event.Action()&MouseClick != 0 {
l.Lock()
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
}
atX, atY := event.Position()
index := l.indexAtPoint(atX, atY)
// Process mouse event.
switch action {
case MouseLeftClick:
setFocus(l)
index := l.indexAtPoint(event.Position())
if index != -1 {
item := l.items[index]
if item.Selected != nil {
l.Unlock()
item.Selected()
l.Lock()
}
if l.selected != nil {
l.Unlock()
l.selected(index, item.MainText, item.SecondaryText, item.Shortcut)
l.Lock()
}
if index != l.currentItem && l.changed != nil {
l.Unlock()
l.changed(index, item.MainText, item.SecondaryText, item.Shortcut)
l.Lock()
}
l.currentItem = index
}
l.Unlock()
consumed = true
case MouseScrollUp:
if l.offset > 0 {
l.offset--
}
consumed = true
case MouseScrollDown:
lines := len(l.items) - l.offset
if l.showSecondaryText {
lines *= 2
}
if _, _, _, height := l.GetInnerRect(); lines > height {
l.offset++
}
consumed = true
}
return
})
}

View File

@ -14,7 +14,7 @@ import (
type Modal struct {
*Box
// The framed embedded in the modal.
// The frame embedded in the modal.
frame *Frame
// The form embedded in the modal's frame.
@ -213,10 +213,15 @@ func (m *Modal) Draw(screen tcell.Screen) {
m.frame.Draw(screen)
}
// GetChildren returns all primitives that have been added.
func (m *Modal) GetChildren() []Primitive {
m.Lock()
defer m.Unlock()
return []Primitive{m.frame}
// 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) {
// Pass mouse events on to the form.
consumed, capture = m.form.MouseHandler()(action, event, setFocus)
if !consumed && action == MouseLeftClick && m.InRect(event.Position()) {
setFocus(m)
consumed = true
}
return
})
}

View File

@ -22,7 +22,7 @@ type page struct {
type Pages struct {
*Box
// The contained pages.
// The contained pages. (Visible) pages are drawn from back to front.
pages []*page
// We keep a reference to the function which allows us to set the focus to
@ -368,18 +368,24 @@ func (p *Pages) Draw(screen tcell.Screen) {
}
}
// GetChildren returns all primitives that have been added.
func (p *Pages) GetChildren() []Primitive {
p.Lock()
defer p.Unlock()
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)
// 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
}
}
return children
// Pass mouse events along to the last visible page item that takes it.
for index := len(p.pages) - 1; index >= 0; index-- {
page := p.pages[index]
if page.Visible {
consumed, capture = page.Item.MouseHandler()(action, event, setFocus)
if consumed {
return
}
}
}
return
})
}

View File

@ -44,16 +44,14 @@ type Primitive interface {
// GetFocusable returns the item's Focusable.
GetFocusable() Focusable
// GetChildren returns all child primitives that have been added.
GetChildren() []Primitive
// 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.
// A value of nil may also be returned to stop the downward propagation of
// mouse events.
//
// 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)
MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive)
}

View File

@ -290,6 +290,13 @@ type Table struct {
// The number of visible rows the last time the table was drawn.
visibleRows int
// The indices of the visible columns as of the last time the table was drawn.
visibleColumnIndices []int
// The net widths of the visible columns as of the last time the table was
// drawn.
visibleColumnWidths []int
// Visibility of the scroll bar.
scrollBarVisibility ScrollBarVisibility
@ -454,8 +461,8 @@ func (t *Table) GetSelection() (row, column int) {
// Select sets the selected cell. Depending on the selection settings
// specified via SetSelectable(), this may be an entire row or column, or even
// ignored completely. The "selection changed" event is fired if such a callback
// is available (even if the selection ends up being the same as before, even if
// cells are not selectable).
// is available (even if the selection ends up being the same as before and even
// if cells are not selectable).
func (t *Table) Select(row, column int) *Table {
t.Lock()
defer t.Unlock()
@ -677,6 +684,49 @@ func (t *Table) GetColumnCount() int {
return t.lastColumn + 1
}
// cellAt returns the row and column located at the given screen coordinates.
// Each returned value may be negative if there is no row and/or cell. This
// function will also process coordinates outside the table's inner rectangle so
// callers will need to check for bounds themselves.
func (t *Table) cellAt(x, y int) (row, column int) {
rectX, rectY, _, _ := t.GetInnerRect()
// Determine row as seen on screen.
if t.borders {
row = (y - rectY - 1) / 2
} else {
row = y - rectY
}
// Respect fixed rows and row offset.
if row >= 0 {
if row >= t.fixedRows {
row += t.rowOffset
}
if row >= len(t.cells) {
row = -1
}
}
// Saerch for the clicked column.
column = -1
if x >= rectX {
columnX := rectX
if t.borders {
columnX++
}
for index, width := range t.visibleColumnWidths {
columnX += width + 1
if x < columnX {
column = t.visibleColumnIndices[index]
break
}
}
}
return
}
// ScrollToBeginning scrolls the table to the beginning to that the top left
// corner of the table is shown. Note that this position may be corrected if
// there is a selection.
@ -978,8 +1028,8 @@ ColumnLoop:
cell.x, cell.y, cell.width = x+columnX+1, y+rowY, finalWidth
_, printed := printWithStyle(screen, cell.Text, x+columnX+1, y+rowY, finalWidth, cell.Align, tcell.StyleDefault.Foreground(cell.Color)|tcell.Style(cell.Attributes))
if TaggedStringWidth(cell.Text)-printed > 0 && printed > 0 {
_, _, style, _ := screen.GetContent(x+columnX+1+finalWidth-1, y+rowY)
printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+1+finalWidth-1, y+rowY, 1, AlignLeft, style)
_, _, style, _ := screen.GetContent(x+columnX+finalWidth, y+rowY)
printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+finalWidth, y+rowY, 1, AlignLeft, style)
}
}
@ -1150,6 +1200,9 @@ ColumnLoop:
}
}
}
// Remember column infos.
t.visibleColumnIndices, t.visibleColumnWidths = columns, widths
}
// InputHandler returns the handler for this primitive.
@ -1378,3 +1431,31 @@ func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primi
}
})
}
// MouseHandler returns the mouse handler for this primitive.
func (t *Table) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
x, y := event.Position()
if !t.InRect(x, y) {
return false, nil
}
switch action {
case MouseLeftClick:
if t.rowsSelectable || t.columnsSelectable {
t.Select(t.cellAt(x, y))
}
consumed = true
setFocus(t)
case MouseScrollUp:
t.trackEnd = false
t.rowOffset--
consumed = true
case MouseScrollDown:
t.rowOffset++
consumed = true
}
return
})
}

View File

@ -35,6 +35,16 @@ type textViewIndex struct {
Region string // The starting region ID.
}
// textViewRegion contains information about a region.
type textViewRegion struct {
// The region ID.
ID string
// The starting and end screen position of the region as determined the last
// time Draw() was called. A negative value indicates out-of-rect positions.
FromX, FromY, ToX, ToY int
}
// TextView is a box which displays text. It implements the io.Writer interface
// so you can stream text to it. This does not trigger a redraw automatically
// but if a handler is installed via SetChangedFunc(), you can cause it to be
@ -104,6 +114,9 @@ type TextView struct {
// The text alignment, one of AlignLeft, AlignCenter, or AlignRight.
align int
// Information about visible regions as of the last call to Draw().
regionInfos []*textViewRegion
// Indices into the "index" slice which correspond to the first line of the
// first highlight and the last line of the last highlight. This is calculated
// during re-indexing. Set to -1 if there is no current highlight.
@ -161,6 +174,10 @@ type TextView struct {
// highlight(s) into the visible screen.
scrollToHighlights bool
// If true, setting new highlights will be a XOR instead of an overwrite
// operation.
toggleHighlights bool
// An optional function which is called when the content of the text view has
// changed.
changed func()
@ -169,6 +186,10 @@ type TextView struct {
// following keys: Escape, Enter, Tab, Backtab.
done func(tcell.Key)
// An optional function which is called when one or more regions were
// highlighted.
highlighted func(added, removed, remaining []string)
sync.RWMutex
}
@ -358,6 +379,18 @@ func (t *TextView) SetDoneFunc(handler func(key tcell.Key)) *TextView {
return t
}
// SetHighlightedFunc sets a handler which is called when the list of currently
// highlighted regions change. It receives a list of region IDs which were newly
// highlighted, those that are not highlighted anymore, and those that remain
// highlighted.
//
// Note that because regions are only determined during drawing, this function
// can only fire for regions that have existed during the last call to Draw().
func (t *TextView) SetHighlightedFunc(handler func(added, removed, remaining []string)) *TextView {
t.highlighted = handler
return t
}
// ScrollTo scrolls to the specified row and column (both starting with 0).
func (t *TextView) ScrollTo(row, column int) *TextView {
t.Lock()
@ -426,18 +459,59 @@ func (t *TextView) clear() *TextView {
return t
}
// Highlight specifies which regions should be highlighted. See class
// description for details on regions. Empty region strings are ignored.
// Highlight specifies which regions should be highlighted. If highlight
// toggling is set to true (see SetToggleHighlights()), the highlight of the
// provided regions is toggled (highlighted regions are un-highlighted and vice
// versa). If toggling is set to false, the provided regions are highlighted and
// all other regions will not be highlighted (you may also provide nil to turn
// off all highlights).
//
// For more information on regions, see class description. Empty region strings
// are ignored.
//
// Text in highlighted regions will be drawn inverted, i.e. with their
// background and foreground colors swapped.
//
// Calling this function will remove any previous highlights. To remove all
// highlights, call this function without any arguments.
func (t *TextView) Highlight(regionIDs ...string) *TextView {
t.Lock()
defer t.Unlock()
// Toggle highlights.
if t.toggleHighlights {
var newIDs []string
HighlightLoop:
for regionID := range t.highlights {
for _, id := range regionIDs {
if regionID == id {
continue HighlightLoop
}
}
newIDs = append(newIDs, regionID)
}
for _, regionID := range regionIDs {
if _, ok := t.highlights[regionID]; !ok {
newIDs = append(newIDs, regionID)
}
}
regionIDs = newIDs
} // Now we have a list of region IDs that end up being highlighted.
// Determine added and removed regions.
var added, removed, remaining []string
if t.highlighted != nil {
for _, regionID := range regionIDs {
if _, ok := t.highlights[regionID]; ok {
remaining = append(remaining, regionID)
delete(t.highlights, regionID)
} else {
added = append(added, regionID)
}
}
for regionID := range t.highlights {
removed = append(removed, regionID)
}
}
// Make new selection.
t.highlights = make(map[string]struct{})
for _, id := range regionIDs {
if id == "" {
@ -446,6 +520,12 @@ func (t *TextView) Highlight(regionIDs ...string) *TextView {
t.highlights[id] = struct{}{}
}
t.index = nil
// Notify.
if t.highlighted != nil && len(added) > 0 || len(removed) > 0 {
t.highlighted(added, removed, remaining)
}
return t
}
@ -460,6 +540,15 @@ func (t *TextView) GetHighlights() (regionIDs []string) {
return
}
// SetToggleHighlights sets a flag to determine how regions are highlighted.
// When set to true, the Highlight() function (or a mouse click) will toggle the
// provided/selected regions. When set to false, Highlight() (or a mouse click)
// will simply highlight the provided regions.
func (t *TextView) SetToggleHighlights(toggle bool) *TextView {
t.toggleHighlights = toggle
return t
}
// ScrollToHighlight will cause the visible area to be scrolled so that the
// highlighted regions appear in the visible area of the text view. This
// repositioning happens the next time the text view is drawn. It happens only
@ -833,6 +922,9 @@ func (t *TextView) Draw(screen tcell.Screen) {
// Re-index.
t.reindexBuffer(width)
if t.regions {
t.regionInfos = nil
}
// If we don't have an index, there's nothing to draw.
if t.index == nil {
@ -917,6 +1009,15 @@ func (t *TextView) Draw(screen tcell.Screen) {
backgroundColor := index.BackgroundColor
attributes := index.Attributes
regionID := index.Region
if t.regions && regionID != "" && (len(t.regionInfos) == 0 || t.regionInfos[len(t.regionInfos)-1].ID != regionID) {
t.regionInfos = append(t.regionInfos, &textViewRegion{
ID: regionID,
FromX: x,
FromY: y + line - t.lineOffset,
ToX: -1,
ToY: -1,
})
}
// Process tags.
colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedText, _ := decomposeString(text, t.dynamicColors, t.regions)
@ -936,82 +1037,99 @@ func (t *TextView) Draw(screen tcell.Screen) {
}
// Print the line.
var colorPos, regionPos, escapePos, tagOffset, skipped int
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
// Process tags.
for {
if colorPos < len(colorTags) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
// Get the color.
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos])
tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
colorPos++
} else if regionPos < len(regionIndices) && textPos+tagOffset >= regionIndices[regionPos][0] && textPos+tagOffset < regionIndices[regionPos][1] {
// Get the region.
regionID = regions[regionPos][1]
tagOffset += regionIndices[regionPos][1] - regionIndices[regionPos][0]
regionPos++
} else {
break
}
}
// Skip the second-to-last character of an escape tag.
if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
tagOffset++
escapePos++
}
// Mix the existing style with the new style.
_, _, existingStyle, _ := screen.GetContent(x+posX, y+line-t.lineOffset)
_, background, _ := existingStyle.Decompose()
style := overlayStyle(background, defaultStyle, foregroundColor, backgroundColor, attributes)
// Do we highlight this character?
var highlighted bool
if len(regionID) > 0 {
if _, ok := t.highlights[regionID]; ok {
highlighted = true
}
}
if highlighted {
fg, bg, _ := style.Decompose()
if bg == tcell.ColorDefault {
r, g, b := fg.RGB()
c := colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255}
_, _, li := c.Hcl()
if li < .5 {
bg = tcell.ColorWhite
if y+line-t.lineOffset >= 0 {
var colorPos, regionPos, escapePos, tagOffset, skipped int
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
// Process tags.
for {
if colorPos < len(colorTags) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
// Get the color.
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos])
tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
colorPos++
} else if regionPos < len(regionIndices) && textPos+tagOffset >= regionIndices[regionPos][0] && textPos+tagOffset < regionIndices[regionPos][1] {
// Get the region.
if regionID != "" && len(t.regionInfos) > 0 && t.regionInfos[len(t.regionInfos)-1].ID == regionID {
// End last region.
t.regionInfos[len(t.regionInfos)-1].ToX = x + posX
t.regionInfos[len(t.regionInfos)-1].ToY = y + line - t.lineOffset
}
regionID = regions[regionPos][1]
if regionID != "" {
// Start new region.
t.regionInfos = append(t.regionInfos, &textViewRegion{
ID: regionID,
FromX: x + posX,
FromY: y + line - t.lineOffset,
ToX: -1,
ToY: -1,
})
}
tagOffset += regionIndices[regionPos][1] - regionIndices[regionPos][0]
regionPos++
} else {
bg = tcell.ColorBlack
break
}
}
style = style.Background(fg).Foreground(bg)
}
// Skip to the right.
if !t.wrap && skipped < skip {
skipped += screenWidth
return false
}
// Stop at the right border.
if posX+screenWidth > width {
return true
}
// Draw the character.
for offset := screenWidth - 1; offset >= 0; offset-- {
if offset == 0 {
screen.SetContent(x+posX+offset, y+line-t.lineOffset, main, comb, style)
} else {
screen.SetContent(x+posX+offset, y+line-t.lineOffset, ' ', nil, style)
// Skip the second-to-last character of an escape tag.
if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
tagOffset++
escapePos++
}
}
// Advance.
posX += screenWidth
return false
})
// Mix the existing style with the new style.
_, _, existingStyle, _ := screen.GetContent(x+posX, y+line-t.lineOffset)
_, background, _ := existingStyle.Decompose()
style := overlayStyle(background, defaultStyle, foregroundColor, backgroundColor, attributes)
// Do we highlight this character?
var highlighted bool
if regionID != "" {
if _, ok := t.highlights[regionID]; ok {
highlighted = true
}
}
if highlighted {
fg, bg, _ := style.Decompose()
if bg == tcell.ColorDefault {
r, g, b := fg.RGB()
c := colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255}
_, _, li := c.Hcl()
if li < .5 {
bg = tcell.ColorWhite
} else {
bg = tcell.ColorBlack
}
}
style = style.Background(fg).Foreground(bg)
}
// Skip to the right.
if !t.wrap && skipped < skip {
skipped += screenWidth
return false
}
// Stop at the right border.
if posX+screenWidth > width {
return true
}
// Draw the character.
for offset := screenWidth - 1; offset >= 0; offset-- {
if offset == 0 {
screen.SetContent(x+posX+offset, y+line-t.lineOffset, main, comb, style)
} else {
screen.SetContent(x+posX+offset, y+line-t.lineOffset, ' ', nil, style)
}
}
// Advance.
posX += screenWidth
return false
})
}
}
// If this view is not scrollable, we'll purge the buffer of lines that have
@ -1090,3 +1208,41 @@ func (t *TextView) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
}
})
}
// MouseHandler returns the mouse handler for this primitive.
func (t *TextView) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
x, y := event.Position()
if !t.InRect(x, y) {
return false, nil
}
switch action {
case MouseLeftClick:
if t.regions {
// Find a region to highlight.
for _, region := range t.regionInfos {
if y == region.FromY && x < region.FromX ||
y == region.ToY && x >= region.ToX ||
region.FromY >= 0 && y < region.FromY ||
region.ToY >= 0 && y > region.ToY {
continue
}
t.Highlight(region.ID)
break
}
}
consumed = true
setFocus(t)
case MouseScrollUp:
t.trackEnd = false
t.lineOffset--
consumed = true
case MouseScrollDown:
t.lineOffset++
consumed = true
}
return
})
}

View File

@ -911,3 +911,41 @@ func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
t.process()
})
}
// MouseHandler returns the mouse handler for this primitive.
func (t *TreeView) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
x, y := event.Position()
if !t.InRect(x, y) {
return false, nil
}
switch action {
case MouseLeftClick:
_, rectY, _, _ := t.GetInnerRect()
y -= rectY
if y >= 0 && y < len(t.nodes) {
node := t.nodes[y]
if node.selectable {
if t.currentNode != node && t.changed != nil {
t.changed(node)
}
if t.selected != nil {
t.selected(node)
}
t.currentNode = node
}
}
consumed = true
setFocus(t)
case MouseScrollUp:
t.movement = treeUp
consumed = true
case MouseScrollDown:
t.movement = treeDown
consumed = true
}
return
})
}