diff --git a/application.go b/application.go index 470ef1a..bbf51b4 100644 --- a/application.go +++ b/application.go @@ -15,6 +15,34 @@ const ( 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 + WheelUp + WheelDown + WheelLeft + WheelRight +) + // queuedUpdate represented the execution of f queued by // Application.QueueUpdate(). The "done" channel receives exactly one element // after f has executed. @@ -52,7 +80,7 @@ type Application struct { // Whether or not the application resizes the root primitive. rootFullscreen bool - // Enable mouse events? + // Set to true if mouse events are enabled. enableMouse bool // An optional capture function which receives a key event and returns the @@ -85,13 +113,11 @@ type Application struct { // be forwarded). mouseCapture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction) - // An optional mouse capture Primitive returned from the MouseHandler. - mouseHandlerCapture Primitive - - lastMouseX, lastMouseY int // track last mouse pos - mouseDownX, mouseDownY int // track last mouse down pos - lastClickTime time.Time - lastMouseBtn tcell.ButtonMask + 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. @@ -123,11 +149,11 @@ func (a *Application) GetInputCapture() func(event *tcell.EventKey) *tcell.Event return a.inputCapture } -// SetMouseCapture sets a function which captures mouse events before they are -// forwarded to the appropriate mouse event handler. -// This function can then choose to forward that event (or a -// different one) by returning it or stop the event processing by returning -// nil. +// 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 @@ -334,8 +360,7 @@ EventLoop: if consumed { a.draw() } - // Keep state: - a.lastMouseBtn = event.Buttons() + a.lastMouseButtons = event.Buttons() if isMouseDownAction { a.mouseDownX, a.mouseDownY = event.Position() } @@ -355,64 +380,64 @@ EventLoop: return nil } -// fireMouseActions determines each mouse action from mouse events -// and fires the appropriate mouse handlers and mouse captures. +// 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) { - a.RLock() - root := a.root - mouseCapture := a.mouseCapture - a.RUnlock() + // We want to relay follow-up events to the same target primitive. + var targetPrimitive Primitive - // Fire a mouse action. - mouseEv := func(action MouseAction) { + // Helper function to fire a mouse action. + fire := func(action MouseAction) { switch action { case MouseLeftDown, MouseMiddleDown, MouseRightDown: isMouseDownAction = true } // Intercept event. - if mouseCapture != nil { - event, action = mouseCapture(event, action) + if a.mouseCapture != nil { + event, action = a.mouseCapture(event, action) if event == nil { consumed = true return // Don't forward event. } } - var handlerTarget Primitive - if a.mouseHandlerCapture != nil { // Check if already captured. - handlerTarget = a.mouseHandlerCapture + // 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 { - handlerTarget = root + primitive = a.root } - - var newHandlerCapture Primitive // None by default. - if handlerTarget != nil { - if handler := handlerTarget.MouseHandler(); handler != nil { - hconsumed := false - hconsumed, newHandlerCapture = handler(action, event, func(p Primitive) { + if primitive != nil { + if handler := primitive.MouseHandler(); handler != nil { + var wasConsumed bool + wasConsumed, capturingPrimitive = handler(action, event, func(p Primitive) { a.SetFocus(p) }) - if hconsumed { + if wasConsumed { consumed = true } } } - a.mouseHandlerCapture = newHandlerCapture + a.mouseCapturingPrimitive = capturingPrimitive } - atX, atY := event.Position() - ebuttons := event.Buttons() - clickMoved := atX != a.mouseDownX || atY != a.mouseDownY - btnDiff := ebuttons ^ a.lastMouseBtn + x, y := event.Position() + buttons := event.Buttons() + clickMoved := x != a.mouseDownX || y != a.mouseDownY + buttonChanges := buttons ^ a.lastMouseButtons - if atX != a.lastMouseX || atY != a.lastMouseY { - mouseEv(MouseMove) - a.lastMouseX = atX - a.lastMouseY = atY + if x != a.lastMouseX || y != a.lastMouseY { + fire(MouseMove) + a.lastMouseX = x + a.lastMouseY = y } - for _, x := range []struct { + for _, buttonEvent := range []struct { button tcell.ButtonMask down, up, click, dclick MouseAction }{ @@ -420,32 +445,34 @@ func (a *Application) fireMouseActions(event *tcell.EventMouse) (consumed, isMou {tcell.Button2, MouseMiddleDown, MouseMiddleUp, MouseMiddleClick, MouseMiddleDoubleClick}, {tcell.Button3, MouseRightDown, MouseRightUp, MouseRightClick, MouseRightDoubleClick}, } { - if btnDiff&x.button != 0 { - if ebuttons&x.button != 0 { - mouseEv(x.down) + if buttonChanges&buttonEvent.button != 0 { + if buttons&buttonEvent.button != 0 { + fire(buttonEvent.down) } else { - mouseEv(x.up) + fire(buttonEvent.up) if !clickMoved { - if a.lastClickTime.Add(DoubleClickInterval).Before(time.Now()) { - mouseEv(x.click) - a.lastClickTime = time.Now() + if a.lastMouseClick.Add(DoubleClickInterval).Before(time.Now()) { + fire(buttonEvent.click) + a.lastMouseClick = time.Now() } else { - mouseEv(x.dclick) - a.lastClickTime = time.Time{} // reset + fire(buttonEvent.dclick) + a.lastMouseClick = time.Time{} // reset } } } } } - for _, x := range []struct { + for _, wheelEvent := range []struct { button tcell.ButtonMask action MouseAction }{ - {tcell.WheelUp, WheelUp}, {tcell.WheelDown, WheelDown}, - {tcell.WheelLeft, WheelLeft}, {tcell.WheelRight, WheelRight}} { - if ebuttons&x.button != 0 { - mouseEv(x.action) + {tcell.WheelUp, WheelUp}, + {tcell.WheelDown, WheelDown}, + {tcell.WheelLeft, WheelLeft}, + {tcell.WheelRight, WheelRight}} { + if buttons&wheelEvent.button != 0 { + fire(wheelEvent.action) } } @@ -699,30 +726,3 @@ func (a *Application) QueueEvent(event tcell.Event) *Application { a.events <- event return a } - -// MouseAction indicates one of the actions the mouse is logically doing. -type MouseAction int16 - -const ( - MouseMove MouseAction = iota - MouseLeftDown - MouseLeftUp - MouseLeftClick - MouseLeftDoubleClick - MouseMiddleDown - MouseMiddleUp - MouseMiddleClick - MouseMiddleDoubleClick - MouseRightDown - MouseRightUp - MouseRightClick - MouseRightDoubleClick - WheelUp - WheelDown - WheelLeft - WheelRight -) - -// DoubleClickInterval specifies the maximum time between clicks -// to register a double click rather than click. -var DoubleClickInterval = 500 * time.Millisecond diff --git a/box.go b/box.go index cede8a6..1870597 100644 --- a/box.go +++ b/box.go @@ -61,8 +61,8 @@ 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). + // 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) } @@ -199,8 +199,8 @@ 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(MouseAction, *tcell.EventMouse, func(p Primitive)) (bool, Primitive)) func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { @@ -217,14 +217,21 @@ func (b *Box) WrapMouseHandler(mouseHandler func(MouseAction, *tcell.EventMouse, // MouseHandler returns nil. func (b *Box) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { - return b.WrapMouseHandler(nil) + 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(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse)) *Box { @@ -232,10 +239,11 @@ func (b *Box) SetMouseCapture(capture func(action MouseAction, event *tcell.Even return b } -func (b *Box) InRect(atX, atY int) bool { - x, y, w, h := b.GetRect() - return atX >= x && atX < x+w && - atY >= y && atY < y+h +// 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 diff --git a/button.go b/button.go index 153c814..fd7c234 100644 --- a/button.go +++ b/button.go @@ -142,12 +142,16 @@ func (b *Button) MouseHandler() func(action MouseAction, event *tcell.EventMouse 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 true, nil + + return }) } diff --git a/checkbox.go b/checkbox.go index bd8286d..7c4b505 100644 --- a/checkbox.go +++ b/checkbox.go @@ -205,16 +205,22 @@ func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr // MouseHandler returns the mouse handler for this primitive. func (c *Checkbox) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return c.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { - if !c.InRect(event.Position()) { + x, y := event.Position() + _, rectY, _, _ := c.GetInnerRect() + if !c.InRect(x, y) { return false, nil } + // Process mouse event. - if action == MouseLeftClick { + if action == MouseLeftClick && y == rectY { + setFocus(c) c.checked = !c.checked if c.changed != nil { c.changed(c.checked) } + consumed = true } - return true, nil + + return }) } diff --git a/demos/button/main.go b/demos/button/main.go index 429b5be..10e52f9 100644 --- a/demos/button/main.go +++ b/demos/button/main.go @@ -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) } } diff --git a/demos/checkbox/main.go b/demos/checkbox/main.go index ef9ef8e..0a6bc70 100644 --- a/demos/checkbox/main.go +++ b/demos/checkbox/main.go @@ -6,7 +6,7 @@ import "github.com/rivo/tview" func main() { app := tview.NewApplication() 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) } } diff --git a/demos/dropdown/main.go b/demos/dropdown/main.go index be9db66..3d9510b 100644 --- a/demos/dropdown/main.go +++ b/demos/dropdown/main.go @@ -8,7 +8,7 @@ func main() { dropdown := tview.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) } } diff --git a/demos/flex/main.go b/demos/flex/main.go index 1c22172..2df2466 100644 --- a/demos/flex/main.go +++ b/demos/flex/main.go @@ -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("Bottom (5 rows)"), 5, 1, false), 0, 2, 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) } } diff --git a/demos/form/main.go b/demos/form/main.go index e64614f..1b50c13 100644 --- a/demos/form/main.go +++ b/demos/form/main.go @@ -18,7 +18,7 @@ func main() { app.Stop() }) 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) } } diff --git a/demos/frame/main.go b/demos/frame/main.go index 9b03e64..db6e184 100644 --- a/demos/frame/main.go +++ b/demos/frame/main.go @@ -16,7 +16,7 @@ func main() { AddText("Header second middle", true, tview.AlignCenter, tcell.ColorRed). AddText("Footer 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) } } diff --git a/demos/grid/main.go b/demos/grid/main.go index cbdb1df..57efe1e 100644 --- a/demos/grid/main.go +++ b/demos/grid/main.go @@ -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 := tview.NewApplication().SetRoot(grid, true).Run(); err != nil { + if err := tview.NewApplication().SetRoot(grid, true).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/demos/inputfield/main.go b/demos/inputfield/main.go index b5ccd77..923bf2d 100644 --- a/demos/inputfield/main.go +++ b/demos/inputfield/main.go @@ -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) } } diff --git a/demos/list/main.go b/demos/list/main.go index 07afa28..402703a 100644 --- a/demos/list/main.go +++ b/demos/list/main.go @@ -15,8 +15,7 @@ func main() { AddItem("Quit", "Press to exit", 'q', func() { app.Stop() }) - app.EnableMouse(true) - if err := app.SetRoot(list, true).Run(); err != nil { + if err := app.SetRoot(list, true).EnableMouse(true).Run(); err != nil { panic(err) } } diff --git a/demos/modal/main.go b/demos/modal/main.go index 893230d..e7ef1f8 100644 --- a/demos/modal/main.go +++ b/demos/modal/main.go @@ -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) } } diff --git a/demos/pages/main.go b/demos/pages/main.go index 8eb67ab..67c06e0 100644 --- a/demos/pages/main.go +++ b/demos/pages/main.go @@ -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) } } diff --git a/demos/presentation/main.go b/demos/presentation/main.go index 1861780..7ae2eb9 100644 --- a/demos/presentation/main.go +++ b/demos/presentation/main.go @@ -91,10 +91,8 @@ func main() { return event }) - app.EnableMouse(true) - // 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) } } diff --git a/dropdown.go b/dropdown.go index d67aa84..12d04ed 100644 --- a/dropdown.go +++ b/dropdown.go @@ -79,6 +79,8 @@ type DropDown struct { // A callback function which is called when the user changes the drop-down's // selection. selected func(text string, index int) + + dragging bool // Set to true when mouse dragging is in progress. } // NewDropDown returns a new drop-down. @@ -417,8 +419,8 @@ 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 is 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 { @@ -427,17 +429,23 @@ func (d *DropDown) evalPrefix() { return } } - // Prefix does not match any item. Remove last rune. + + // Prefix does not match any item. Remove last rune. TODO: Use uniseg here. r := []rune(d.prefix) d.prefix = string(r[:len(r)-1]) } } -// Hand control over to the list. +// 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) @@ -465,11 +473,15 @@ func (d *DropDown) openList(setFocus func(Primitive)) { } 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() { @@ -493,39 +505,44 @@ func (d *DropDown) HasFocus() bool { return d.hasFocus } -func (d *DropDown) listClick(event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool) { - if !d.list.InRect(event.Position()) { - return false - } - // Mouse is within the list. - if handler := d.list.MouseHandler(); handler != nil { - consumed, _ := handler(MouseLeftClick, event, setFocus) - return consumed - } - return false -} - // MouseHandler returns the mouse handler for this primitive. func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return d.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { - inRect := d.InRect(event.Position()) // mouse in the dropdown box itself, not the list. + // 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 false, nil + return d.InRect(x, y), nil // No, and it's not expanded either. Ignore. } - // Process mouse event. - if action == MouseLeftClick { - if !d.open { // not open + + // 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) - } else { // if d.open - if !inRect { - d.listClick(event, setFocus) - } - d.closeList(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 } } - if d.open { - return true, d // capture - } - return inRect, nil + + return }) } diff --git a/flex.go b/flex.go index e991b4f..108b5b0 100644 --- a/flex.go +++ b/flex.go @@ -208,13 +208,15 @@ func (f *Flex) MouseHandler() func(action MouseAction, event *tcell.EventMouse, if !f.InRect(event.Position()) { return false, nil } - // Process mouse event. + + // 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 consumed, capture + return } } - return true, nil + + return }) } diff --git a/form.go b/form.go index 6bdd6bc..ca123bf 100644 --- a/form.go +++ b/form.go @@ -626,19 +626,32 @@ func (f *Form) MouseHandler() func(action MouseAction, event *tcell.EventMouse, if !f.InRect(event.Position()) { return false, nil } - // Process mouse event. + + // Determine items to pass mouse events to. for _, item := range f.items { consumed, capture = item.MouseHandler()(action, event, setFocus) if consumed { - return consumed, capture + return } } for _, button := range f.buttons { consumed, capture = button.MouseHandler()(action, event, setFocus) if consumed { - return consumed, capture + 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 true, nil + + return }) } diff --git a/frame.go b/frame.go index 436b4e4..1ea9d6e 100644 --- a/frame.go +++ b/frame.go @@ -12,8 +12,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://github.com/rivo/tview/wiki/Frame for an example. type Frame struct { @@ -162,11 +162,8 @@ func (f *Frame) MouseHandler() func(action MouseAction, event *tcell.EventMouse, if !f.InRect(event.Position()) { return false, nil } - // Process mouse event. - consumed, capture = f.primitive.MouseHandler()(action, event, setFocus) - if consumed { - return consumed, capture - } - return true, nil + + // Pass mouse events on to contained primitive. + return f.primitive.MouseHandler()(action, event, setFocus) }) } diff --git a/grid.go b/grid.go index 5f24e4f..b814d35 100644 --- a/grid.go +++ b/grid.go @@ -667,13 +667,15 @@ func (g *Grid) MouseHandler() func(action MouseAction, event *tcell.EventMouse, if !g.InRect(event.Position()) { return false, nil } - // Process mouse event. + + // 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 consumed, capture + return } } - return true, nil + + return }) } diff --git a/inputfield.go b/inputfield.go index 856b150..784a06b 100644 --- a/inputfield.go +++ b/inputfield.go @@ -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,9 @@ type InputField struct { // A callback function set by the Form class and called when the user leaves // this form item. 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. @@ -326,6 +326,7 @@ func (i *InputField) Draw(screen tcell.Screen) { } // Draw input area. + i.fieldX = x fieldWidth := i.fieldWidth if fieldWidth == 0 { fieldWidth = math.MaxInt32 @@ -595,13 +596,30 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p // MouseHandler returns the mouse handler for this primitive. func (i *InputField) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return i.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { - if !i.InRect(event.Position()) { + x, y := event.Position() + _, rectY, _, _ := i.GetInnerRect() + if !i.InRect(x, y) { return false, nil } + // Process mouse event. - if action == MouseLeftDown { + 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 true, nil + + return }) } diff --git a/list.go b/list.go index 600dc72..474477b 100644 --- a/list.go +++ b/list.go @@ -559,22 +559,23 @@ 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 -1 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 } - if n >= len(l.items) { + if index >= len(l.items) { return -1 } - return n + return index } // MouseHandler returns the mouse handler for this primitive. @@ -583,10 +584,11 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, if !l.InRect(event.Position()) { return false, nil } + // Process mouse event. if action == MouseLeftClick { - atX, atY := event.Position() - index := l.indexAtPoint(atX, atY) + setFocus(l) + index := l.indexAtPoint(event.Position()) if index != -1 { item := l.items[index] if item.Selected != nil { @@ -600,7 +602,9 @@ func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, } l.currentItem = index } + consumed = true } - return true, nil + + return }) } diff --git a/modal.go b/modal.go index 4256cdf..f534113 100644 --- a/modal.go +++ b/modal.go @@ -12,7 +12,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. @@ -179,14 +179,12 @@ func (m *Modal) Draw(screen tcell.Screen) { // MouseHandler returns the mouse handler for this primitive. func (m *Modal) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return m.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { - if !m.InRect(event.Position()) { - return false, nil + // 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 } - // Process mouse event. - consumed, capture = m.frame.MouseHandler()(action, event, setFocus) - if consumed { - return consumed, capture - } - return true, nil + return }) } diff --git a/pages.go b/pages.go index 9854658..0955045 100644 --- a/pages.go +++ b/pages.go @@ -20,7 +20,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 @@ -285,15 +285,18 @@ func (p *Pages) MouseHandler() func(action MouseAction, event *tcell.EventMouse, if !p.InRect(event.Position()) { return false, nil } - // Process mouse event. - for _, page := range p.pages { + + // 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 consumed, capture + return } } } - return true, nil + + return }) } diff --git a/primitive.go b/primitive.go index e226149..416d708 100644 --- a/primitive.go +++ b/primitive.go @@ -47,7 +47,8 @@ type Primitive interface { // 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