mirror of https://github.com/mum4k/termdash.git
214 lines
5.7 KiB
Go
214 lines
5.7 KiB
Go
// Copyright 2018 Google Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package container
|
|
|
|
// focus.go contains code that tracks the focused container.
|
|
|
|
import (
|
|
"image"
|
|
|
|
"github.com/mum4k/termdash/mouse"
|
|
"github.com/mum4k/termdash/private/button"
|
|
"github.com/mum4k/termdash/terminal/terminalapi"
|
|
)
|
|
|
|
// pointCont finds the top-most (on the screen) container whose area contains
|
|
// the given point. Returns nil if none of the containers in the tree contain
|
|
// this point.
|
|
func pointCont(c *Container, p image.Point) *Container {
|
|
var (
|
|
errStr string
|
|
cont *Container
|
|
)
|
|
postOrder(rootCont(c), &errStr, visitFunc(func(c *Container) error {
|
|
if p.In(c.area) && cont == nil {
|
|
cont = c
|
|
}
|
|
return nil
|
|
}))
|
|
return cont
|
|
}
|
|
|
|
// focusTracker tracks the active (focused) container.
|
|
// This is not thread-safe, the implementation assumes that the owner of
|
|
// focusTracker performs locking.
|
|
type focusTracker struct {
|
|
// container is the currently focused container.
|
|
container *Container
|
|
|
|
// candidate is the container that might become focused next. I.e. we got
|
|
// a mouse click and now waiting for a release or a timeout.
|
|
candidate *Container
|
|
|
|
// buttonFSM is a state machine tracking mouse clicks in containers and
|
|
// moving focus from one container to the next.
|
|
buttonFSM *button.FSM
|
|
}
|
|
|
|
// newFocusTracker returns a new focus tracker with focus set at the provided
|
|
// container.
|
|
func newFocusTracker(c *Container) *focusTracker {
|
|
return &focusTracker{
|
|
container: c,
|
|
// Mouse FSM tracking clicks inside the entire area for the root
|
|
// container.
|
|
buttonFSM: button.NewFSM(mouse.ButtonLeft, c.area),
|
|
}
|
|
}
|
|
|
|
// active returns container that is currently active.
|
|
func (ft *focusTracker) active() *Container {
|
|
return ft.container
|
|
}
|
|
|
|
// isActive determines if the provided container is the currently active container.
|
|
func (ft *focusTracker) isActive(c *Container) bool {
|
|
return ft.container == c
|
|
}
|
|
|
|
// setActive sets the currently active container to the one provided.
|
|
func (ft *focusTracker) setActive(c *Container) {
|
|
ft.container = c
|
|
}
|
|
|
|
// next moves focus to the next container.
|
|
// If group is not nil, focus will only move between containers with a matching
|
|
// focus group number.
|
|
func (ft *focusTracker) next(group *FocusGroup) {
|
|
var (
|
|
errStr string
|
|
firstCont *Container
|
|
nextCont *Container
|
|
focusNext bool
|
|
)
|
|
preOrder(rootCont(ft.container), &errStr, visitFunc(func(c *Container) error {
|
|
if nextCont != nil {
|
|
// Already found the next container, nothing to do.
|
|
return nil
|
|
}
|
|
|
|
if firstCont == nil && c.isLeaf() {
|
|
// Remember the first eligible container in case we "wrap" over,
|
|
// i.e. finish the iteration before finding the next container.
|
|
switch {
|
|
case group == nil && !c.opts.keyFocusSkip:
|
|
fallthrough
|
|
case group != nil && c.inFocusGroup(*group):
|
|
firstCont = c
|
|
}
|
|
}
|
|
|
|
if ft.container == c {
|
|
// Visiting the currently focused container, going to focus the
|
|
// next one.
|
|
focusNext = true
|
|
return nil
|
|
}
|
|
|
|
if focusNext && c.isLeaf() {
|
|
switch {
|
|
case group == nil && !c.opts.keyFocusSkip:
|
|
fallthrough
|
|
case group != nil && c.inFocusGroup(*group):
|
|
nextCont = c
|
|
}
|
|
}
|
|
return nil
|
|
}))
|
|
|
|
if nextCont == nil && firstCont != nil {
|
|
// If the traversal finishes without finding the next container, move
|
|
// focus back to the first container.
|
|
ft.setActive(firstCont)
|
|
} else if nextCont != nil {
|
|
ft.setActive(nextCont)
|
|
}
|
|
}
|
|
|
|
// previous moves focus to the previous container.
|
|
// If group is not nil, focus will only move between containers with a matching
|
|
// focus group number.
|
|
func (ft *focusTracker) previous(group *FocusGroup) {
|
|
var (
|
|
errStr string
|
|
prevCont *Container
|
|
lastCont *Container
|
|
visitedCurr bool
|
|
)
|
|
preOrder(rootCont(ft.container), &errStr, visitFunc(func(c *Container) error {
|
|
if ft.container == c {
|
|
visitedCurr = true
|
|
}
|
|
|
|
if c.isLeaf() {
|
|
switch {
|
|
case group == nil && !c.opts.keyFocusSkip:
|
|
fallthrough
|
|
case group != nil && c.inFocusGroup(*group):
|
|
if !visitedCurr {
|
|
// Remember the last eligible container closest to the one
|
|
// currently focused.
|
|
prevCont = c
|
|
}
|
|
lastCont = c
|
|
}
|
|
}
|
|
return nil
|
|
}))
|
|
|
|
if prevCont != nil {
|
|
ft.setActive(prevCont)
|
|
} else if lastCont != nil {
|
|
ft.setActive(lastCont)
|
|
}
|
|
}
|
|
|
|
// mouse identifies mouse events that change the focused container and track
|
|
// the focused container in the tree.
|
|
// The argument c is the container onto which the mouse event landed.
|
|
func (ft *focusTracker) mouse(target *Container, m *terminalapi.Mouse) {
|
|
clicked, bs := ft.buttonFSM.Event(m)
|
|
switch {
|
|
case bs == button.Down:
|
|
ft.candidate = target
|
|
case bs == button.Up && clicked:
|
|
if target == ft.candidate {
|
|
ft.container = target
|
|
}
|
|
}
|
|
}
|
|
|
|
// updateArea updates the area that the focus tracker considers active for
|
|
// mouse clicks.
|
|
func (ft *focusTracker) updateArea(ar image.Rectangle) {
|
|
ft.buttonFSM.UpdateArea(ar)
|
|
}
|
|
|
|
// reachableFrom asserts whether the currently focused container is reachable
|
|
// from the provided node in the tree.
|
|
func (ft *focusTracker) reachableFrom(node *Container) bool {
|
|
var (
|
|
errStr string
|
|
reachable bool
|
|
)
|
|
preOrder(node, &errStr, visitFunc(func(c *Container) error {
|
|
if c == ft.container {
|
|
reachable = true
|
|
}
|
|
return nil
|
|
}))
|
|
return reachable
|
|
}
|