diff --git a/application.go b/application.go index 05a4357..a212691 100644 --- a/application.go +++ b/application.go @@ -38,6 +38,9 @@ type Application struct { // Whether or not the application resizes the root primitive. rootFullscreen bool + // Enable mouse events? + enableMouse bool + // 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 // be forwarded). @@ -62,6 +65,33 @@ type Application struct { // (screen.Init() and draw() will be called implicitly). A value of nil will // stop the application. 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 EventMouse) EventMouse +} + +// EventKey is the key input event info. +// This exists for some consistency with EventMouse, +// even though it's just an alias to *tcell.EventKey for backwards compatibility. +type EventKey = *tcell.EventKey + +// EventMouse is the mouse event info. +type EventMouse struct { + *tcell.EventMouse + Target Primitive + Application *Application +} + +// IsZero returns true if this is a zero object. +func (e EventMouse) IsZero() bool { + return e == EventMouse{} +} + +// SetFocus will set focus to the primitive. +func (e EventMouse) SetFocus(p Primitive) { + e.Application.SetFocus(p) } // NewApplication creates and returns a new application. @@ -93,6 +123,22 @@ func (a *Application) GetInputCapture() func(event *tcell.EventKey) *tcell.Event return a.inputCapture } +// SetMouseCapture sets a function which captures mouse events before they are +// forwarded to the appropriate mouse event handler. +// This function can then choose to forward that event (or a +// different one) by returning it or stop the event processing by returning +// nil. +func (a *Application) SetMouseCapture(capture func(event EventMouse) EventMouse) *Application { + a.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 EventMouse) EventMouse { + return a.mouseCapture +} + // SetScreen allows you to provide your own tcell.Screen object. For most // applications, this is not needed and you should be familiar with // tcell.Screen when using this function. @@ -121,6 +167,13 @@ func (a *Application) SetScreen(screen tcell.Screen) *Application { return a } +// EnableMouse enables mouse events. +func (a *Application) EnableMouse() { + a.Lock() + a.enableMouse = true + a.Unlock() +} + // Run starts the application and thus the event loop. This function returns // when Stop() was called. func (a *Application) Run() error { @@ -138,6 +191,9 @@ func (a *Application) Run() error { a.Unlock() return err } + if a.enableMouse { + a.screen.EnableMouse() + } } // We catch panics to clean up because they mess up the terminal. @@ -207,13 +263,15 @@ EventLoop: break EventLoop } + a.RLock() + p := a.focus + inputCapture := a.inputCapture + mouseCapture := a.mouseCapture + screen := a.screen + a.RUnlock() + switch event := event.(type) { case *tcell.EventKey: - a.RLock() - p := a.focus - inputCapture := a.inputCapture - a.RUnlock() - // Intercept keys. if inputCapture != nil { event = inputCapture(event) @@ -238,14 +296,32 @@ EventLoop: } } case *tcell.EventResize: - a.RLock() - screen := a.screen - a.RUnlock() if screen == nil { continue } screen.Clear() a.draw() + case *tcell.EventMouse: + atX, atY := event.Position() + ptarget := a.GetPrimitiveAtPoint(atX, atY) // p under mouse. + if ptarget == nil { + ptarget = p // Fallback to focused. + } + event2 := EventMouse{event, ptarget, a} + + // Intercept event. + if mouseCapture != nil { + event2 = mouseCapture(event2) + if event2.IsZero() { + a.draw() + continue // Don't forward event. + } + } + + if handler := ptarget.MouseHandler(); handler != nil { + handler(event2) + a.draw() + } } // If we have updates, now is the time to execute them. @@ -261,6 +337,34 @@ EventLoop: return nil } +func findAtPoint(atX, atY int, p Primitive) Primitive { + x, y, w, h := p.GetRect() + if atX < x || atY < y { + return nil + } + if atX >= x+w || atY >= y+h { + return nil + } + bestp := p + for _, pchild := range p.GetChildren() { + x := findAtPoint(atX, atY, pchild) + if x != nil { + // Always overwrite if we find another one, + // this is because if any overlap, the last one is "on top". + bestp = x + } + } + return bestp +} + +// GetPrimitiveAtPoint returns the Primitive at the specified point, or nil. +// Note that this only works with a valid hierarchy of primitives (children) +func (a *Application) GetPrimitiveAtPoint(atX, atY int) Primitive { + a.RLock() + defer a.RUnlock() + return findAtPoint(atX, atY, a.root) +} + // Stop stops the application, causing Run() to return. func (a *Application) Stop() { a.Lock() diff --git a/box.go b/box.go index 809b3c9..ff11586 100644 --- a/box.go +++ b/box.go @@ -58,6 +58,11 @@ type Box struct { // 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) + + // An optional capture function which receives a mouse event and returns the + // event to be forwarded to the primitive's default mouse event handler (nil if + // nothing should be forwarded). + mouseCapture func(event EventMouse) EventMouse } // NewBox returns a Box without a border. @@ -192,6 +197,45 @@ func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey { return b.inputCapture } +// 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. +// +// This is only meant to be used by subclassing primitives. +func (b *Box) WrapMouseHandler(mouseHandler func(EventMouse)) func(EventMouse) { + return func(event EventMouse) { + if b.mouseCapture != nil { + event = b.mouseCapture(event) + } + if !event.IsZero() && mouseHandler != nil { + mouseHandler(event) + } + } +} + +// MouseHandler returns nil. +func (b *Box) MouseHandler() func(event EventMouse) { + return b.WrapMouseHandler(nil) +} + +// 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. +// +// Providing a nil handler will remove a previously existing handler. +func (b *Box) SetMouseCapture(capture func(EventMouse) EventMouse) *Box { + b.mouseCapture = capture + return b +} + +// GetMouseCapture returns the function installed with SetMouseCapture() or nil +// if no such function has been installed. +func (b *Box) GetMouseCapture() func(EventMouse) EventMouse { + return b.mouseCapture +} + // SetBackgroundColor sets the box's background color. func (b *Box) SetBackgroundColor(color tcell.Color) *Box { b.backgroundColor = color @@ -353,3 +397,8 @@ func (b *Box) HasFocus() bool { func (b *Box) GetFocusable() Focusable { return b.focus } + +// GetChildren gets the children. +func (b *Box) GetChildren() []Primitive { + return nil +} diff --git a/button.go b/button.go index d3a6aed..eb8a676 100644 --- a/button.go +++ b/button.go @@ -135,3 +135,15 @@ func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Prim } }) } + +// InputHandler returns the handler for this primitive. +func (b *Button) MouseHandler() func(event EventMouse) { + return b.WrapMouseHandler(func(event EventMouse) { + // Process mouse event. + if event.Buttons()&tcell.Button1 != 0 { + if b.selected != nil { + b.selected() + } + } + }) +} diff --git a/flex.go b/flex.go index 56cbc75..660d67d 100644 --- a/flex.go +++ b/flex.go @@ -195,3 +195,11 @@ func (f *Flex) HasFocus() bool { } return false } + +func (f *Flex) GetChildren() []Primitive { + children := make([]Primitive, len(f.items)) + for i, item := range f.items { + children[i] = item.Item + } + return children +} diff --git a/form.go b/form.go index 8b9e77a..47051cb 100644 --- a/form.go +++ b/form.go @@ -600,3 +600,17 @@ func (f *Form) focusIndex() int { } return -1 } + +func (f *Form) GetChildren() []Primitive { + children := make([]Primitive, len(f.items)+len(f.buttons)) + i := 0 + for _, item := range f.items { + children[i] = item + i++ + } + for _, button := range f.buttons { + children[i] = button + i++ + } + return children +} diff --git a/frame.go b/frame.go index 77c5316..99d092a 100644 --- a/frame.go +++ b/frame.go @@ -155,3 +155,7 @@ func (f *Frame) HasFocus() bool { } return false } + +func (f *Frame) GetChildren() []Primitive { + return []Primitive{f.primitive} +} diff --git a/grid.go b/grid.go index 2de0f0c..70d0303 100644 --- a/grid.go +++ b/grid.go @@ -660,3 +660,11 @@ func (g *Grid) Draw(screen tcell.Screen) { } } } + +func (g *Grid) GetChildren() []Primitive { + children := make([]Primitive, len(g.items)) + for i, item := range g.items { + children[i] = item.Item + } + return children +} diff --git a/modal.go b/modal.go index f359a14..c0707b1 100644 --- a/modal.go +++ b/modal.go @@ -169,3 +169,7 @@ func (m *Modal) Draw(screen tcell.Screen) { m.frame.SetRect(x, y, width, height) m.frame.Draw(screen) } + +func (m *Modal) GetChildren() []Primitive { + return []Primitive{m.frame} +} diff --git a/pages.go b/pages.go index 155da73..35a1d9d 100644 --- a/pages.go +++ b/pages.go @@ -278,3 +278,15 @@ func (p *Pages) Draw(screen tcell.Screen) { page.Item.Draw(screen) } } + +func (p *Pages) GetChildren() []Primitive { + var children []Primitive + for _, page := range p.pages { + // Considering invisible pages as not children. + // Even though we track all the pages, not all are "children" currently. + if page.Visible { + children = append(children, page.Item) + } + } + return children +} diff --git a/primitive.go b/primitive.go index 88a9d46..6c7a6c7 100644 --- a/primitive.go +++ b/primitive.go @@ -43,4 +43,17 @@ type Primitive interface { // GetFocusable returns the item's Focusable. GetFocusable() Focusable + + // GetChildren gets the children. + GetChildren() []Primitive + + // MouseHandler returns a handler which receives mouse events. + // It is called by the Application class. + // + // A zero value of EventMouse{} may also be returned to stop propagation. + // + // The Box class provides functionality to intercept mouse events. If you + // subclass from Box, it is recommended that you wrap your handler using + // Box.WrapMouseHandler() so you inherit that functionality. + MouseHandler() func(event EventMouse) }