Merge pull request #422 from rivo/mouse

Added mouse support, closes #13
pull/423/head
rivo 4 years ago committed by GitHub
commit 7cc182c584
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,6 +1,6 @@
# Rich Interactive Widgets for Terminal UIs # Rich Interactive Widgets for Terminal UIs
[![Godoc Reference](https://img.shields.io/badge/godoc-reference-blue.svg)](https://godoc.org/github.com/rivo/tview) [![Godoc Reference](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/rivo/tview)
[![Go Report](https://img.shields.io/badge/go%20report-A%2B-brightgreen.svg)](https://goreportcard.com/report/github.com/rivo/tview) [![Go Report](https://img.shields.io/badge/go%20report-A%2B-brightgreen.svg)](https://goreportcard.com/report/github.com/rivo/tview)
This Go package provides commonly needed components for terminal based user interfaces. This Go package provides commonly needed components for terminal based user interfaces.
@ -60,51 +60,3 @@ This package is based on [github.com/gdamore/tcell](https://github.com/gdamore/t
## Your Feedback ## Your Feedback
Add your issue here on GitHub. Feel free to get in touch if you have any questions. Add your issue here on GitHub. Feel free to get in touch if you have any questions.
## Version History
(There are no corresponding tags in the project. I only keep such a history in this README.)
- v0.20 (2019-07-08)
- Added autocomplete functionality to `InputField`.
- v0.19 (2018-10-28)
- Added `QueueUpdate()` and `QueueEvent()` to `Application` to help with modifications to primitives from goroutines.
- v0.18 (2018-10-18)
- `InputField` elements can now be navigated freely.
- v0.17 (2018-06-20)
- Added `TreeView`.
- v0.15 (2018-05-02)
- `Flex` and `Grid` don't clear their background per default, thus allowing for custom modals. See the [Wiki](https://github.com/rivo/tview/wiki/Modal) for an example.
- v0.14 (2018-04-13)
- Added an `Escape()` function which keep strings like color or region tags from being recognized as such.
- Added `ANSIWriter()` and `TranslateANSI()` which convert ANSI escape sequences to `tview` color tags.
- v0.13 (2018-04-01)
- Added background colors and text attributes to color tags.
- v0.12 (2018-03-13)
- Added "suspended mode" to `Application`.
- v0.11 (2018-03-02)
- Added a `RemoveItem()` function to `Grid` and `Flex`.
- v0.10 (2018-02-22)
- Direct access to the `screen` object through callback in `Box` (i.e. for all primitives).
- v0.9 (2018-02-20)
- Introduced `Grid` layout.
- Direct access to the `screen` object through callbacks in `Application`.
- v0.8 (2018-01-17)
- Color tags can now be used almost everywhere.
- v0.7 (2018-01-16)
- Forms can now also have a horizontal layout.
- v0.6 (2018-01-14)
- All primitives can now intercept all key events when they have focus.
- Key events can also be intercepted globally (changed to a more general, consistent handling)
- v0.5 (2018-01-13)
- `TextView` now has word wrapping and text alignment
- v0.4 (2018-01-12)
- `TextView` now accepts color tags with any W3C color (including RGB hex values).
- Support for wide unicode characters.
- v0.3 (2018-01-11)
- Added masking to `InputField` and password entry to `Form`.
- v0.2 (2018-01-10)
- Added `Styles` variable with default colors for primitives.
- Completed some missing InputField functions.
- v0.1 (2018-01-06)
- First Release.

@ -15,6 +15,34 @@ const (
redrawPause = 50 * time.Millisecond redrawPause = 50 * 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 // queuedUpdate represented the execution of f queued by
// Application.QueueUpdate(). The "done" channel receives exactly one element // Application.QueueUpdate(). The "done" channel receives exactly one element
// after f has executed. // after f has executed.
@ -52,6 +80,9 @@ type Application struct {
// Whether or not the application resizes the root primitive. // Whether or not the application resizes the root primitive.
rootFullscreen bool rootFullscreen bool
// Set to true if mouse events are enabled.
enableMouse bool
// An optional capture function which receives a key event and returns the // An optional capture function which receives a key event and returns the
// event to be forwarded to the default input handler (nil if nothing should // event to be forwarded to the default input handler (nil if nothing should
// be forwarded). // be forwarded).
@ -76,6 +107,17 @@ type Application struct {
// (screen.Init() and draw() will be called implicitly). A value of nil will // (screen.Init() and draw() will be called implicitly). A value of nil will
// stop the application. // stop the application.
screenReplacement chan tcell.Screen screenReplacement chan tcell.Screen
// An optional capture function which receives a mouse event and returns the
// event to be forwarded to the default mouse handler (nil if nothing should
// be forwarded).
mouseCapture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction)
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.
} }
// NewApplication creates and returns a new application. // NewApplication creates and returns a new application.
@ -107,6 +149,22 @@ func (a *Application) GetInputCapture() func(event *tcell.EventKey) *tcell.Event
return a.inputCapture return a.inputCapture
} }
// SetMouseCapture sets a function which captures mouse events (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 a nil mouse event.
func (a *Application) SetMouseCapture(capture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction)) *Application {
a.mouseCapture = capture
return a
}
// GetMouseCapture returns the function installed with SetMouseCapture() or nil
// if no such function has been installed.
func (a *Application) GetMouseCapture() func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction) {
return a.mouseCapture
}
// SetScreen allows you to provide your own tcell.Screen object. For most // SetScreen allows you to provide your own tcell.Screen object. For most
// applications, this is not needed and you should be familiar with // applications, this is not needed and you should be familiar with
// tcell.Screen when using this function. // tcell.Screen when using this function.
@ -135,6 +193,21 @@ func (a *Application) SetScreen(screen tcell.Screen) *Application {
return a return a
} }
// EnableMouse enables mouse events.
func (a *Application) EnableMouse(enable bool) *Application {
a.Lock()
defer a.Unlock()
if enable != a.enableMouse && a.screen != nil {
if enable {
a.screen.EnableMouse()
} else {
a.screen.DisableMouse()
}
}
a.enableMouse = enable
return a
}
// Run starts the application and thus the event loop. This function returns // Run starts the application and thus the event loop. This function returns
// when Stop() was called. // when Stop() was called.
func (a *Application) Run() error { func (a *Application) Run() error {
@ -157,6 +230,9 @@ func (a *Application) Run() error {
a.Unlock() a.Unlock()
return err return err
} }
if a.enableMouse {
a.screen.EnableMouse()
}
} }
// We catch panics to clean up because they mess up the terminal. // We catch panics to clean up because they mess up the terminal.
@ -279,6 +355,15 @@ EventLoop:
lastRedraw = time.Now() lastRedraw = time.Now()
screen.Clear() screen.Clear()
a.draw() a.draw()
case *tcell.EventMouse:
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. // If we have updates, now is the time to execute them.
@ -295,6 +380,105 @@ EventLoop:
return nil 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
}
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
}
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
}
}
}
}
}
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 consumed, isMouseDownAction
}
// Stop stops the application, causing Run() to return. // Stop stops the application, causing Run() to return.
func (a *Application) Stop() { func (a *Application) Stop() {
a.Lock() a.Lock()

@ -59,6 +59,11 @@ type Box struct {
// An optional function which is called before the box is drawn. // An optional function which is called before the box is drawn.
draw func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) draw func(screen tcell.Screen, x, y, width, height int) (int, int, int, int)
// An optional capture function which receives a mouse event and returns the
// event to be forwarded to the primitive's default mouse event handler (at
// least one nil if nothing should be forwarded).
mouseCapture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse)
} }
// NewBox returns a Box without a border. // NewBox returns a Box without a border.
@ -193,6 +198,60 @@ func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey {
return b.inputCapture return b.inputCapture
} }
// WrapMouseHandler wraps a mouse event handler (see MouseHandler()) with the
// functionality to capture 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(MouseAction, *tcell.EventMouse, func(p Primitive)) (bool, Primitive)) func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if b.mouseCapture != nil {
action, event = b.mouseCapture(action, event)
}
if event != nil && mouseHandler != nil {
consumed, capture = mouseHandler(action, event, setFocus)
}
return
}
}
// MouseHandler returns nil.
func (b *Box) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return b.WrapMouseHandler(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 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(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(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse) {
return b.mouseCapture
}
// SetBackgroundColor sets the box's background color. // SetBackgroundColor sets the box's background color.
func (b *Box) SetBackgroundColor(color tcell.Color) *Box { func (b *Box) SetBackgroundColor(color tcell.Color) *Box {
b.backgroundColor = color b.backgroundColor = color

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

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

@ -9,7 +9,7 @@ func main() {
app.Stop() app.Stop()
}) })
button.SetBorder(true).SetRect(0, 0, 22, 3) 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) panic(err)
} }
} }

@ -6,7 +6,7 @@ import "github.com/rivo/tview"
func main() { func main() {
app := tview.NewApplication() app := tview.NewApplication()
checkbox := tview.NewCheckbox().SetLabel("Hit Enter to check box: ") checkbox := tview.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) panic(err)
} }
} }

@ -8,7 +8,7 @@ func main() {
dropdown := tview.NewDropDown(). dropdown := tview.NewDropDown().
SetLabel("Select an option (hit Enter): "). SetLabel("Select an option (hit Enter): ").
SetOptions([]string{"First", "Second", "Third", "Fourth", "Fifth"}, nil) 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) panic(err)
} }
} }

@ -14,7 +14,7 @@ func main() {
AddItem(tview.NewBox().SetBorder(true).SetTitle("Middle (3 x height of Top)"), 0, 3, false). AddItem(tview.NewBox().SetBorder(true).SetTitle("Middle (3 x height of Top)"), 0, 3, false).
AddItem(tview.NewBox().SetBorder(true).SetTitle("Bottom (5 rows)"), 5, 1, false), 0, 2, false). AddItem(tview.NewBox().SetBorder(true).SetTitle("Bottom (5 rows)"), 5, 1, false), 0, 2, false).
AddItem(tview.NewBox().SetBorder(true).SetTitle("Right (20 cols)"), 20, 1, false) AddItem(tview.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) panic(err)
} }
} }

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

@ -16,7 +16,7 @@ func main() {
AddText("Header second middle", true, tview.AlignCenter, tcell.ColorRed). AddText("Header second middle", true, tview.AlignCenter, tcell.ColorRed).
AddText("Footer middle", false, tview.AlignCenter, tcell.ColorGreen). AddText("Footer middle", false, tview.AlignCenter, tcell.ColorGreen).
AddText("Footer second middle", false, tview.AlignCenter, tcell.ColorGreen) AddText("Footer second middle", false, tview.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) panic(err)
} }
} }

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

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

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

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

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

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

@ -47,33 +47,36 @@ func main() {
End, End,
} }
pages := tview.NewPages()
// The bottom row has some info on where we are. // The bottom row has some info on where we are.
info := tview.NewTextView(). info := tview.NewTextView().
SetDynamicColors(true). SetDynamicColors(true).
SetRegions(true). SetRegions(true).
SetWrap(false) SetWrap(false).
SetHighlightedFunc(func(added, removed, remaining []string) {
pages.SwitchToPage(added[0])
})
// Create the pages for all slides. // Create the pages for all slides.
currentSlide := 0
info.Highlight(strconv.Itoa(currentSlide))
pages := tview.NewPages()
previousSlide := func() { previousSlide := func() {
currentSlide = (currentSlide - 1 + len(slides)) % len(slides) slide, _ := strconv.Atoi(info.GetHighlights()[0])
info.Highlight(strconv.Itoa(currentSlide)). slide = (slide - 1 + len(slides)) % len(slides)
info.Highlight(strconv.Itoa(slide)).
ScrollToHighlight() ScrollToHighlight()
pages.SwitchToPage(strconv.Itoa(currentSlide))
} }
nextSlide := func() { nextSlide := func() {
currentSlide = (currentSlide + 1) % len(slides) slide, _ := strconv.Atoi(info.GetHighlights()[0])
info.Highlight(strconv.Itoa(currentSlide)). slide = (slide + 1) % len(slides)
info.Highlight(strconv.Itoa(slide)).
ScrollToHighlight() ScrollToHighlight()
pages.SwitchToPage(strconv.Itoa(currentSlide))
} }
for index, slide := range slides { for index, slide := range slides {
title, primitive := slide(nextSlide) 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) fmt.Fprintf(info, `%d ["%d"][darkcyan]%s[white][""] `, index+1, index, title)
} }
info.Highlight("0")
// Create the main layout. // Create the main layout.
layout := tview.NewFlex(). layout := tview.NewFlex().
@ -92,7 +95,7 @@ func main() {
}) })
// Start the application. // 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) panic(err)
} }
} }

@ -39,7 +39,7 @@ func main() {
table.GetCell(row, column).SetTextColor(tcell.ColorRed) table.GetCell(row, column).SetTextColor(tcell.ColorRed)
table.SetSelectable(false, false) 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) panic(err)
} }
} }

@ -63,7 +63,7 @@ func main() {
} }
}) })
textView.SetBorder(true) 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) panic(err)
} }
} }

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

@ -79,6 +79,8 @@ type DropDown struct {
// A callback function which is called when the user changes the drop-down's // A callback function which is called when the user changes the drop-down's
// selection. // selection.
selected func(text string, index int) selected func(text string, index int)
dragging bool // Set to true when mouse dragging is in progress.
} }
// NewDropDown returns a new drop-down. // NewDropDown returns a new drop-down.
@ -394,22 +396,6 @@ func (d *DropDown) Draw(screen tcell.Screen) {
// InputHandler returns the handler for this primitive. // InputHandler returns the handler for this primitive.
func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
// A helper function which selects an item in the drop-down list based on
// the current prefix.
evalPrefix := func() {
if len(d.prefix) > 0 {
for index, option := range d.options {
if strings.HasPrefix(strings.ToLower(option.Text), d.prefix) {
d.list.SetCurrentItem(index)
return
}
}
// Prefix does not match any item. Remove last rune.
r := []rune(d.prefix)
d.prefix = string(r[:len(r)-1])
}
}
// Process key event. // Process key event.
switch key := event.Key(); key { switch key := event.Key(); key {
case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown: case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown:
@ -418,45 +404,10 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
// If the first key was a letter already, it becomes part of the prefix. // If the first key was a letter already, it becomes part of the prefix.
if r := event.Rune(); key == tcell.KeyRune && r != ' ' { if r := event.Rune(); key == tcell.KeyRune && r != ' ' {
d.prefix += string(r) d.prefix += string(r)
evalPrefix() d.evalPrefix()
} }
// Hand control over to the list. d.openList(setFocus)
d.open = true
optionBefore := d.currentOption
d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) {
// An option was selected. Close the list again.
d.open = false
setFocus(d)
d.currentOption = index
// Trigger "selected" event.
if d.selected != nil {
d.selected(d.options[d.currentOption].Text, d.currentOption)
}
if d.options[d.currentOption].Selected != nil {
d.options[d.currentOption].Selected()
}
}).SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyRune {
d.prefix += string(event.Rune())
evalPrefix()
} else if event.Key() == tcell.KeyBackspace || event.Key() == tcell.KeyBackspace2 {
if len(d.prefix) > 0 {
r := []rune(d.prefix)
d.prefix = string(r[:len(r)-1])
}
evalPrefix()
} else if event.Key() == tcell.KeyEscape {
d.open = false
d.currentOption = optionBefore
setFocus(d)
} else {
d.prefix = ""
}
return event
})
setFocus(d.list)
case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab: case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab:
if d.done != nil { if d.done != nil {
d.done(key) d.done(key)
@ -468,6 +419,75 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
}) })
} }
// 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 {
if strings.HasPrefix(strings.ToLower(option.Text), d.prefix) {
d.list.SetCurrentItem(index)
return
}
}
// Prefix does not match any item. Remove last rune.
r := []rune(d.prefix)
d.prefix = string(r[:len(r)-1])
}
}
// 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)
// Trigger "selected" event.
if d.selected != nil {
d.selected(d.options[d.currentOption].Text, d.currentOption)
}
if d.options[d.currentOption].Selected != nil {
d.options[d.currentOption].Selected()
}
}).SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyRune {
d.prefix += string(event.Rune())
d.evalPrefix()
} else if event.Key() == tcell.KeyBackspace || event.Key() == tcell.KeyBackspace2 {
if len(d.prefix) > 0 {
r := []rune(d.prefix)
d.prefix = string(r[:len(r)-1])
}
d.evalPrefix()
} else if event.Key() == tcell.KeyEscape {
d.currentOption = optionBefore
d.closeList(setFocus)
} else {
d.prefix = ""
}
return event
})
setFocus(d.list)
}
// 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)
}
}
// Focus is called by the application when the primitive receives focus. // Focus is called by the application when the primitive receives focus.
func (d *DropDown) Focus(delegate func(p Primitive)) { func (d *DropDown) Focus(delegate func(p Primitive)) {
d.Box.Focus(delegate) d.Box.Focus(delegate)
@ -483,3 +503,45 @@ func (d *DropDown) HasFocus() bool {
} }
return d.hasFocus return d.hasFocus
} }
// MouseHandler returns the mouse handler for this primitive.
func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return d.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
// 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.
}
// 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
})
}

@ -201,3 +201,22 @@ func (f *Flex) HasFocus() bool {
} }
return false return false
} }
// MouseHandler returns the mouse handler for this primitive.
func (f *Flex) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !f.InRect(event.Position()) {
return false, nil
}
// 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
})
}

@ -619,3 +619,39 @@ func (f *Form) focusIndex() int {
} }
return -1 return -1
} }
// MouseHandler returns the mouse handler for this primitive.
func (f *Form) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !f.InRect(event.Position()) {
return false, nil
}
// 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
})
}

@ -12,8 +12,8 @@ type frameText struct {
Color tcell.Color // The text color. Color tcell.Color // The text color.
} }
// Frame is a wrapper which adds a border around another primitive. The top area // Frame is a wrapper which adds space around another primitive. In addition,
// (header) and the bottom area (footer) may also contain text. // the top area (header) and the bottom area (footer) may also contain text.
// //
// See https://github.com/rivo/tview/wiki/Frame for an example. // See https://github.com/rivo/tview/wiki/Frame for an example.
type Frame struct { type Frame struct {
@ -155,3 +155,15 @@ func (f *Frame) HasFocus() bool {
} }
return false return false
} }
// MouseHandler returns the mouse handler for this primitive.
func (f *Frame) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !f.InRect(event.Position()) {
return false, nil
}
// Pass mouse events on to contained primitive.
return f.primitive.MouseHandler()(action, event, setFocus)
})
}

@ -660,3 +660,22 @@ func (g *Grid) Draw(screen tcell.Screen) {
} }
} }
} }
// MouseHandler returns the mouse handler for this primitive.
func (g *Grid) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return g.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !g.InRect(event.Position()) {
return false, nil
}
// 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
})
}

@ -69,9 +69,6 @@ type InputField struct {
// The cursor position as a byte index into the text string. // The cursor position as a byte index into the text string.
cursorPos int 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 // 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 // input field and returns a slice of strings to be displayed in a drop-down
// selection. // selection.
@ -96,6 +93,9 @@ type InputField struct {
// A callback function set by the Form class and called when the user leaves // A callback function set by the Form class and called when the user leaves
// this form item. // this form item.
finished func(tcell.Key) finished func(tcell.Key)
fieldX int // The x-coordinate of the input field as determined during the last call to Draw().
offset int // The number of bytes of the text string skipped ahead while drawing.
} }
// NewInputField returns a new input field. // NewInputField returns a new input field.
@ -326,6 +326,7 @@ func (i *InputField) Draw(screen tcell.Screen) {
} }
// Draw input area. // Draw input area.
i.fieldX = x
fieldWidth := i.fieldWidth fieldWidth := i.fieldWidth
if fieldWidth == 0 { if fieldWidth == 0 {
fieldWidth = math.MaxInt32 fieldWidth = math.MaxInt32
@ -591,3 +592,34 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
} }
}) })
} }
// MouseHandler returns the mouse handler for this primitive.
func (i *InputField) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return i.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
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
})
}

@ -558,3 +558,69 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
} }
}) })
} }
// 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
}
index := y - rectY
if l.showSecondaryText {
index /= 2
}
index += l.offset
if index >= len(l.items) {
return -1
}
return index
}
// MouseHandler returns the mouse handler for this primitive.
func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return l.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !l.InRect(event.Position()) {
return false, nil
}
// Process mouse event.
switch action {
case MouseLeftClick:
setFocus(l)
index := l.indexAtPoint(event.Position())
if index != -1 {
item := l.items[index]
if item.Selected != nil {
item.Selected()
}
if l.selected != nil {
l.selected(index, item.MainText, item.SecondaryText, item.Shortcut)
}
if index != l.currentItem && l.changed != nil {
l.changed(index, item.MainText, item.SecondaryText, item.Shortcut)
}
l.currentItem = index
}
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
})
}

@ -12,7 +12,7 @@ import (
type Modal struct { type Modal struct {
*Box *Box
// The framed embedded in the modal. // The frame embedded in the modal.
frame *Frame frame *Frame
// The form embedded in the modal's frame. // The form embedded in the modal's frame.
@ -175,3 +175,16 @@ func (m *Modal) Draw(screen tcell.Screen) {
m.frame.SetRect(x, y, width, height) m.frame.SetRect(x, y, width, height)
m.frame.Draw(screen) m.frame.Draw(screen)
} }
// 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
})
}

@ -20,7 +20,7 @@ type page struct {
type Pages struct { type Pages struct {
*Box *Box
// The contained pages. // The contained pages. (Visible) pages are drawn from back to front.
pages []*page pages []*page
// We keep a reference to the function which allows us to set the focus to // We keep a reference to the function which allows us to set the focus to
@ -278,3 +278,25 @@ func (p *Pages) Draw(screen tcell.Screen) {
page.Item.Draw(screen) page.Item.Draw(screen)
} }
} }
// MouseHandler returns the mouse handler for this primitive.
func (p *Pages) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return p.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !p.InRect(event.Position()) {
return false, nil
}
// 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
})
}

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

@ -251,6 +251,13 @@ type Table struct {
// The number of visible rows the last time the table was drawn. // The number of visible rows the last time the table was drawn.
visibleRows int 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
// The style of the selected rows. If this value is 0, selected rows are // The style of the selected rows. If this value is 0, selected rows are
// simply inverted. // simply inverted.
selectedStyle tcell.Style selectedStyle tcell.Style
@ -360,8 +367,8 @@ func (t *Table) GetSelection() (row, column int) {
// Select sets the selected cell. Depending on the selection settings // Select sets the selected cell. Depending on the selection settings
// specified via SetSelectable(), this may be an entire row or column, or even // 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 // 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 // is available (even if the selection ends up being the same as before and even
// cells are not selectable). // if cells are not selectable).
func (t *Table) Select(row, column int) *Table { func (t *Table) Select(row, column int) *Table {
t.selectedRow, t.selectedColumn = row, column t.selectedRow, t.selectedColumn = row, column
if t.selectionChanged != nil { if t.selectionChanged != nil {
@ -537,6 +544,49 @@ func (t *Table) GetColumnCount() int {
return t.lastColumn + 1 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 // 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 // corner of the table is shown. Note that this position may be corrected if
// there is a selection. // there is a selection.
@ -824,8 +874,8 @@ ColumnLoop:
cell.x, cell.y, cell.width = x+columnX+1, y+rowY, finalWidth 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)) _, 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 { if TaggedStringWidth(cell.Text)-printed > 0 && printed > 0 {
_, _, style, _ := screen.GetContent(x+columnX+1+finalWidth-1, y+rowY) _, _, style, _ := screen.GetContent(x+columnX+finalWidth, y+rowY)
printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+1+finalWidth-1, y+rowY, 1, AlignLeft, style) printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+finalWidth, y+rowY, 1, AlignLeft, style)
} }
} }
@ -964,6 +1014,9 @@ ColumnLoop:
} }
} }
} }
// Remember column infos.
t.visibleColumnIndices, t.visibleColumnWidths = columns, widths
} }
// InputHandler returns the handler for this primitive. // InputHandler returns the handler for this primitive.
@ -1173,3 +1226,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
})
}

@ -36,6 +36,16 @@ type textViewIndex struct {
Region string // The starting region ID. 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 // 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 // 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 // but if a handler is installed via SetChangedFunc(), you can cause it to be
@ -106,6 +116,9 @@ type TextView struct {
// The text alignment, one of AlignLeft, AlignCenter, or AlignRight. // The text alignment, one of AlignLeft, AlignCenter, or AlignRight.
align int 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 // 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 // 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. // during re-indexing. Set to -1 if there is no current highlight.
@ -163,6 +176,10 @@ type TextView struct {
// highlight(s) into the visible screen. // highlight(s) into the visible screen.
scrollToHighlights bool 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 // An optional function which is called when the content of the text view has
// changed. // changed.
changed func() changed func()
@ -170,6 +187,10 @@ type TextView struct {
// An optional function which is called when the user presses one of the // An optional function which is called when the user presses one of the
// following keys: Escape, Enter, Tab, Backtab. // following keys: Escape, Enter, Tab, Backtab.
done func(tcell.Key) done func(tcell.Key)
// An optional function which is called when one or more regions were
// highlighted.
highlighted func(added, removed, remaining []string)
} }
// NewTextView returns a new text view. // NewTextView returns a new text view.
@ -325,6 +346,18 @@ func (t *TextView) SetDoneFunc(handler func(key tcell.Key)) *TextView {
return t 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). // ScrollTo scrolls to the specified row and column (both starting with 0).
func (t *TextView) ScrollTo(row, column int) *TextView { func (t *TextView) ScrollTo(row, column int) *TextView {
if !t.scrollable { if !t.scrollable {
@ -374,15 +407,56 @@ func (t *TextView) Clear() *TextView {
return t return t
} }
// Highlight specifies which regions should be highlighted. See class // Highlight specifies which regions should be highlighted. If highlight
// description for details on regions. Empty region strings are ignored. // 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 // Text in highlighted regions will be drawn inverted, i.e. with their
// background and foreground colors swapped. // 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 { func (t *TextView) Highlight(regionIDs ...string) *TextView {
// 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{}) t.highlights = make(map[string]struct{})
for _, id := range regionIDs { for _, id := range regionIDs {
if id == "" { if id == "" {
@ -391,6 +465,12 @@ func (t *TextView) Highlight(regionIDs ...string) *TextView {
t.highlights[id] = struct{}{} t.highlights[id] = struct{}{}
} }
t.index = nil t.index = nil
// Notify.
if t.highlighted != nil && len(added) > 0 || len(removed) > 0 {
t.highlighted(added, removed, remaining)
}
return t return t
} }
@ -402,6 +482,15 @@ func (t *TextView) GetHighlights() (regionIDs []string) {
return 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 // ScrollToHighlight will cause the visible area to be scrolled so that the
// highlighted regions appear in the visible area of the text view. This // 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 // repositioning happens the next time the text view is drawn. It happens only
@ -766,6 +855,9 @@ func (t *TextView) Draw(screen tcell.Screen) {
// Re-index. // Re-index.
t.reindexBuffer(width) t.reindexBuffer(width)
if t.regions {
t.regionInfos = nil
}
// If we don't have an index, there's nothing to draw. // If we don't have an index, there's nothing to draw.
if t.index == nil { if t.index == nil {
@ -850,6 +942,15 @@ func (t *TextView) Draw(screen tcell.Screen) {
backgroundColor := index.BackgroundColor backgroundColor := index.BackgroundColor
attributes := index.Attributes attributes := index.Attributes
regionID := index.Region 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. // Process tags.
colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedText, _ := decomposeString(text, t.dynamicColors, t.regions) colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedText, _ := decomposeString(text, t.dynamicColors, t.regions)
@ -881,7 +982,22 @@ func (t *TextView) Draw(screen tcell.Screen) {
colorPos++ colorPos++
} else if regionPos < len(regionIndices) && textPos+tagOffset >= regionIndices[regionPos][0] && textPos+tagOffset < regionIndices[regionPos][1] { } else if regionPos < len(regionIndices) && textPos+tagOffset >= regionIndices[regionPos][0] && textPos+tagOffset < regionIndices[regionPos][1] {
// Get the region. // 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] 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] tagOffset += regionIndices[regionPos][1] - regionIndices[regionPos][0]
regionPos++ regionPos++
} else { } else {
@ -902,7 +1018,7 @@ func (t *TextView) Draw(screen tcell.Screen) {
// Do we highlight this character? // Do we highlight this character?
var highlighted bool var highlighted bool
if len(regionID) > 0 { if regionID != "" {
if _, ok := t.highlights[regionID]; ok { if _, ok := t.highlights[regionID]; ok {
highlighted = true highlighted = true
} }
@ -1022,3 +1138,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
})
}

@ -730,3 +730,41 @@ func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
t.process() 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
})
}

Loading…
Cancel
Save