Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proof of concept: add support for opt-in inside-out keyboard event propagation #968

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion application.go
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ EventLoop:
// Pass other key events to the root primitive.
if root != nil && root.HasFocus() {
if handler := root.InputHandler(); handler != nil {
handler(event, func(p Primitive) {
_ = handler(event, func(p Primitive) {
a.SetFocus(p)
})
draw = true
Expand Down
70 changes: 58 additions & 12 deletions box.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ type Box struct {
// nothing should be forwarded).
inputCapture func(event *tcell.EventKey) *tcell.EventKey

// An optional capture function which receives a key event and returns a bool
// which indicates if the event should be potentially further processed by
// parents' own inputCaptureAfter function. This can be used to construct
// an inside-out event handling chain.
// It has a default value of capturing all events, to preserve semantics
// of tview (originally, tview never passed events back to parents).
inputCaptureAfter func(event *tcell.EventKey) *tcell.EventKey

// 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)

Expand All @@ -71,13 +79,14 @@ type Box struct {
// NewBox returns a Box without a border.
func NewBox() *Box {
b := &Box{
width: 15,
height: 10,
innerX: -1, // Mark as uninitialized.
backgroundColor: Styles.PrimitiveBackgroundColor,
borderStyle: tcell.StyleDefault.Foreground(Styles.BorderColor).Background(Styles.PrimitiveBackgroundColor),
titleColor: Styles.TitleColor,
titleAlign: AlignCenter,
width: 15,
height: 10,
innerX: -1, // Mark as uninitialized.
backgroundColor: Styles.PrimitiveBackgroundColor,
borderStyle: tcell.StyleDefault.Foreground(Styles.BorderColor).Background(Styles.PrimitiveBackgroundColor),
titleColor: Styles.TitleColor,
titleAlign: AlignCenter,
inputCaptureAfter: func(event *tcell.EventKey) *tcell.EventKey { return nil },
}
return b
}
Expand Down Expand Up @@ -158,19 +167,29 @@ func (b *Box) GetDrawFunc() func(screen tcell.Screen, x, y, width, height int) (
// on to the provided (default) input handler.
//
// This is only meant to be used by subclassing primitives.
func (b *Box) WrapInputHandler(inputHandler func(*tcell.EventKey, func(p Primitive))) func(*tcell.EventKey, func(p Primitive)) {
return func(event *tcell.EventKey, setFocus func(p Primitive)) {
func (b *Box) WrapInputHandler(inputHandler func(*tcell.EventKey, func(p Primitive)) (consumed bool)) func(*tcell.EventKey, func(p Primitive)) (consumed bool) {
return func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
if b.inputCapture != nil {
event = b.inputCapture(event)
if event == nil {
consumed = true
}
}
if event != nil && inputHandler != nil {
inputHandler(event, setFocus)
if event != nil && !consumed && inputHandler != nil {
consumed = inputHandler(event, setFocus)
}
if event != nil && !consumed && b.inputCaptureAfter != nil {
event = b.inputCaptureAfter(event)
if event == nil {
consumed = true
}
}
return
}
}

// InputHandler returns nil. Box has no default input handling.
func (b *Box) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
func (b *Box) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
return b.WrapInputHandler(nil)
}

Expand Down Expand Up @@ -206,12 +225,39 @@ func (b *Box) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKe
return b
}

// SetInputCaptureAfter installs a function which captures key events after they are
// forwarded to the primitive's default key event handler. This function can
// then choose to consume the event and prevent any further processing by
// returning nil. If a non-nil event is returned, it will be passed back to
// parent, and potentially further up the tree.
//
// Providing a nil handler will remove a previously existing handler. NOTE:
// the default value of this function is a function which captures all
// events, so setting it to nil, will instead configure to capture none.
//
// This function can also be used on container primitives (like Flex, Grid, or
// Form) as keyboard events will likewise just be given back to the parent,
// and processed if it has its own InputCaptureAfter.
//
// Pasted key events are not forwarded to the input capture function if pasting
// is enabled (see [Application.EnablePaste]).
func (b *Box) SetInputCaptureAfter(capture func(event *tcell.EventKey) *tcell.EventKey) *Box {
b.inputCaptureAfter = capture
return b
}

// GetInputCapture returns the function installed with SetInputCapture() or nil
// if no such function has been installed.
func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey {
return b.inputCapture
}

// GetInputCaptureAfter returns the function installed with SetInputCaptureAfter() or nil
// if no such function has been installed.
func (b *Box) GetInputCaptureAfter() func(event *tcell.EventKey) *tcell.EventKey {
return b.inputCaptureAfter
}

// WrapMouseHandler wraps a mouse event handler (see [Box.MouseHandler]) with the
// functionality to capture mouse events (see [Box.SetMouseCapture]) before passing
// them on to the provided (default) event handler.
Expand Down
6 changes: 4 additions & 2 deletions button.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ func (b *Button) Draw(screen tcell.Screen) {
}

// InputHandler returns the handler for this primitive.
func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return b.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
return b.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
if b.disabled {
return
}
Expand All @@ -177,6 +177,8 @@ func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Prim
b.exit(key)
}
}

return
})
}

Expand Down
6 changes: 4 additions & 2 deletions checkbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,8 @@ func (c *Checkbox) Draw(screen tcell.Screen) {
}

// InputHandler returns the handler for this primitive.
func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return c.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
return c.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
if c.disabled {
return
}
Expand All @@ -303,6 +303,8 @@ func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
c.finished(key)
}
}

return
})
}

Expand Down
6 changes: 4 additions & 2 deletions demos/primitive/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ func (r *RadioButtons) Draw(screen tcell.Screen) {
}

// InputHandler returns the handler for this primitive.
func (r *RadioButtons) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
return r.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
func (r *RadioButtons) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) (consumed bool) {
return r.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) (consumed bool) {
switch event.Key() {
case tcell.KeyUp:
r.currentOption--
Expand All @@ -56,6 +56,8 @@ func (r *RadioButtons) InputHandler() func(event *tcell.EventKey, setFocus func(
r.currentOption = len(r.options) - 1
}
}

return
})
}

Expand Down
6 changes: 4 additions & 2 deletions dropdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,8 +448,8 @@ func (d *DropDown) Draw(screen tcell.Screen) {
}

// InputHandler returns the handler for this primitive.
func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
if d.disabled {
return
}
Expand Down Expand Up @@ -482,6 +482,8 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
d.finished(key)
}
}

return
})
}

Expand Down
7 changes: 4 additions & 3 deletions flex.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,16 +247,17 @@ func (f *Flex) MouseHandler() func(action MouseAction, event *tcell.EventMouse,
}

// InputHandler returns the handler for this primitive.
func (f *Flex) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return f.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
func (f *Flex) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
return f.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
for _, item := range f.items {
if item.Item != nil && item.Item.HasFocus() {
if handler := item.Item.InputHandler(); handler != nil {
handler(event, setFocus)
consumed = handler(event, setFocus)
return
}
}
}
return
})
}

Expand Down
10 changes: 6 additions & 4 deletions form.go
Original file line number Diff line number Diff line change
Expand Up @@ -846,12 +846,12 @@ func (f *Form) MouseHandler() func(action MouseAction, event *tcell.EventMouse,
}

// InputHandler returns the handler for this primitive.
func (f *Form) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return f.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
func (f *Form) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
return f.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
for _, item := range f.items {
if item != nil && item.HasFocus() {
if handler := item.InputHandler(); handler != nil {
handler(event, setFocus)
consumed = handler(event, setFocus)
return
}
}
Expand All @@ -860,11 +860,13 @@ func (f *Form) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
for _, button := range f.buttons {
if button.HasFocus() {
if handler := button.InputHandler(); handler != nil {
handler(event, setFocus)
consumed = handler(event, setFocus)
return
}
}
}

return
})
}

Expand Down
7 changes: 4 additions & 3 deletions frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,15 +209,16 @@ func (f *Frame) MouseHandler() func(action MouseAction, event *tcell.EventMouse,
}

// InputHandler returns the handler for this primitive.
func (f *Frame) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return f.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
func (f *Frame) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
return f.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
if f.primitive == nil {
return
}
if handler := f.primitive.InputHandler(); handler != nil {
handler(event, setFocus)
consumed = handler(event, setFocus)
return
}
return
})
}

Expand Down
8 changes: 5 additions & 3 deletions grid.go
Original file line number Diff line number Diff line change
Expand Up @@ -653,14 +653,14 @@ func (g *Grid) MouseHandler() func(action MouseAction, event *tcell.EventMouse,
}

// InputHandler returns the handler for this primitive.
func (g *Grid) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return g.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
func (g *Grid) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
return g.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
if !g.hasFocus {
// Pass event on to child primitive.
for _, item := range g.items {
if item != nil && item.Item.HasFocus() {
if handler := item.Item.InputHandler(); handler != nil {
handler(event, setFocus)
consumed = handler(event, setFocus)
return
}
}
Expand Down Expand Up @@ -698,6 +698,8 @@ func (g *Grid) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
case tcell.KeyRight:
g.columnOffset++
}

return
})
}

Expand Down
8 changes: 5 additions & 3 deletions inputfield.go
Original file line number Diff line number Diff line change
Expand Up @@ -508,8 +508,8 @@ func (i *InputField) Draw(screen tcell.Screen) {
}

// InputHandler returns the handler for this primitive.
func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return i.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
return i.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
if i.textArea.GetDisabled() {
return
}
Expand Down Expand Up @@ -610,8 +610,10 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
fallthrough
default:
// Forward other key events to the text area.
i.textArea.InputHandler()(event, setFocus)
consumed = i.textArea.InputHandler()(event, setFocus)
}

return
})
}

Expand Down
6 changes: 4 additions & 2 deletions list.go
Original file line number Diff line number Diff line change
Expand Up @@ -568,8 +568,8 @@ func (l *List) adjustOffset() {
}

// InputHandler returns the handler for this primitive.
func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return l.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
return l.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
if event.Key() == tcell.KeyEscape {
if l.done != nil {
l.done()
Expand Down Expand Up @@ -663,6 +663,8 @@ func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primit
}
l.adjustOffset()
}

return
})
}

Expand Down
7 changes: 4 additions & 3 deletions modal.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,13 +202,14 @@ func (m *Modal) MouseHandler() func(action MouseAction, event *tcell.EventMouse,
}

// InputHandler returns the handler for this primitive.
func (m *Modal) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return m.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
func (m *Modal) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
return m.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
if m.frame.HasFocus() {
if handler := m.frame.InputHandler(); handler != nil {
handler(event, setFocus)
consumed = handler(event, setFocus)
return
}
}
return
})
}
7 changes: 4 additions & 3 deletions pages.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,16 +303,17 @@ func (p *Pages) MouseHandler() func(action MouseAction, event *tcell.EventMouse,
}

// InputHandler returns the handler for this primitive.
func (p *Pages) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return p.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
func (p *Pages) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
return p.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool) {
for _, page := range p.pages {
if page.Item.HasFocus() {
if handler := page.Item.InputHandler(); handler != nil {
handler(event, setFocus)
consumed = handler(event, setFocus)
return
}
}
}
return
})
}

Expand Down
2 changes: 1 addition & 1 deletion primitive.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type Primitive interface {
// The Box class provides functionality to intercept keyboard input. If you
// subclass from Box, it is recommended that you wrap your handler using
// Box.WrapInputHandler() so you inherit that functionality.
InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive))
InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) (consumed bool)

// Focus is called by the application when the primitive receives focus.
// Implementers may call delegate() to pass the focus on to another primitive.
Expand Down
Loading