/
focus.go
213 lines (190 loc) · 5.69 KB
/
focus.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
// 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
}