-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnavigator.go
More file actions
323 lines (296 loc) · 7.41 KB
/
navigator.go
File metadata and controls
323 lines (296 loc) · 7.41 KB
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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
package ui
// PathSegment represents a single step within the navigator stack.
type PathSegment struct {
Widget Widget
Index int
}
// Path is an ordered collection of PathSegments from root to current focus.
type Path []PathSegment
// Current returns the widget referenced by the last segment in the path.
func (p Path) Current() Widget {
if len(p) == 0 {
return nil
}
return p[len(p)-1].Widget
}
// NavigatorEventType identifies the kind of navigation change.
type NavigatorEventType byte
const (
NavigatorEventFocusChanged NavigatorEventType = iota
NavigatorEventActivated
NavigatorEventDeactivated
)
// NavigatorEvent encapsulates navigation changes for observers.
type NavigatorEvent struct {
Type NavigatorEventType
Path Path
}
// NavigatorObserver consumes navigator events.
type NavigatorObserver interface {
OnNavigatorEvent(NavigatorEvent)
}
type Navigator struct {
stack []Navigable
observers []NavigatorObserver
}
// NewNavigator creates a navigator rooted at the provided Navigable.
func NewNavigator(root Navigable) *Navigator {
if root == nil {
panic("navigator requires root container")
}
return &Navigator{
stack: []Navigable{
root,
},
}
}
func (n *Navigator) AddObserver(obs NavigatorObserver) {
n.observers = append(n.observers, obs)
}
// Depth reports how many navigable containers are currently on the stack.
func (n *Navigator) Depth() int {
return len(n.stack)
}
// Current yields the currently focused widget or the active container when no
// child has focus.
func (n *Navigator) Current() Widget {
item := n.currentContainer().Item()
if item != nil {
return item
}
return n.currentContainer()
}
// Next advances focus to the next selectable widget in the current container.
func (n *Navigator) Next() bool {
container := n.currentContainer()
target := n.findSelectable(container, container.Index()+1, 1)
if target < 0 {
return false
}
return n.focusExact(target)
}
func (n *Navigator) Prev() bool {
container := n.currentContainer()
start := container.Index()
if start < 0 {
start = container.ChildCount() - 1
} else {
start--
}
target := n.findSelectable(container, start, -1)
if target < 0 {
return false
}
return n.focusExact(target)
}
// Focus explicitly sets the selection index inside the current container.
func (n *Navigator) Focus(index int) bool {
container := n.currentContainer()
if index < 0 {
prev := container.Index()
container.SetIndex(-1)
if container.Index() != prev {
n.notify(NavigatorEventFocusChanged)
}
return true
}
target := n.findSelectable(container, index, 1)
if target < 0 {
return false
}
return n.focusExact(target)
}
// Enter activates the selected widget. Navigable children are pushed onto the
// stack, other widgets receive activation events.
func (n *Navigator) Enter() bool {
if !n.ensureSelection() {
return false
}
container := n.currentContainer()
container.SetActive(container.Index())
item := container.Item()
if item == nil {
return false
}
if child, ok := item.(Navigable); ok {
n.stack = append(n.stack, child)
if child.Index() < 0 && child.ChildCount() > 0 {
if first := n.findSelectable(child, 0, 1); first >= 0 {
child.SetIndex(first)
}
}
child.SetActive(child.Index())
n.notify(NavigatorEventFocusChanged)
return true
}
n.notify(NavigatorEventActivated)
return true
}
// Back deactivates the current widget and unwinds to the parent when possible.
func (n *Navigator) Back() bool {
if len(n.stack) == 0 {
return false
}
container := n.currentContainer()
wasActive := container.Active()
container.SetActive(-1)
n.notify(NavigatorEventDeactivated)
if wasActive {
return true
}
if len(n.stack) == 1 {
return true
}
n.stack = n.stack[:len(n.stack)-1]
n.notify(NavigatorEventFocusChanged)
return true
}
// Path returns the navigation stack and current item as a Path.
func (n *Navigator) Path() Path {
path := make(Path, 0, len(n.stack)+1)
for _, container := range n.stack {
path = append(path, PathSegment{
Widget: container,
Index: container.Index(),
})
}
if item := n.currentContainer().Item(); item != nil {
path = append(path, PathSegment{
Widget: item,
Index: -1,
})
}
return path
}
// Walk traverses the entire navigable hierarchy depth-first invoking fn.
func (n *Navigator) Walk(fn func(Path) bool) {
if len(n.stack) == 0 {
return
}
walkContainer(n.stack[0], nil, fn)
}
// walkContainer traverses container depth-first, building paths and invoking fn
// at each node. Returning false aborts traversal.
func walkContainer(container Navigable, path Path, fn func(Path) bool) bool {
currentPath := appendPath(path, PathSegment{
Widget: container,
Index: container.Index(),
})
if !fn(currentPath) {
return false
}
for i := 0; i < container.ChildCount(); i++ {
w := container.Child(i)
if w == nil {
continue
}
childPath := appendPath(currentPath, PathSegment{
Widget: w,
Index: i,
})
if !fn(childPath) {
return false
}
if child, ok := w.(Navigable); ok {
if !walkContainer(child, childPath, fn) {
return false
}
}
}
return true
}
// appendPath produces a new Path containing segment appended to path.
func appendPath(path Path, segment PathSegment) Path {
next := append(Path(nil), path...)
return append(next, segment)
}
// ensureSelection guarantees that the current container points at a selectable
// child before navigation actions are performed.
func (n *Navigator) ensureSelection() bool {
container := n.currentContainer()
if container.ChildCount() == 0 {
return false
}
if container.Index() >= 0 && isSelectable(container.Item()) {
return true
}
target := n.findSelectable(container, 0, 1)
if target < 0 {
return false
}
container.SetIndex(target)
n.notify(NavigatorEventFocusChanged)
return true
}
// currentContainer returns the Navigable instance at the top of the stack.
func (n *Navigator) currentContainer() Navigable {
return n.stack[len(n.stack)-1]
}
// notify emits a NavigatorEvent of type t to every observer.
func (n *Navigator) notify(t NavigatorEventType) {
if len(n.observers) == 0 {
return
}
event := NavigatorEvent{
Type: t,
Path: n.Path(),
}
for _, obs := range n.observers {
obs.OnNavigatorEvent(event)
}
}
// findSelectable searches forward/backward from start for the next selectable
// child index. Returns -1 when no matching widget is found.
func (n *Navigator) findSelectable(container Navigable, start int, direction int) int {
count := container.ChildCount()
if count == 0 {
return -1
}
if direction >= 0 {
if start < 0 {
start = 0
}
for i := start; i < count; i++ {
if w := container.Child(i); isSelectable(w) {
return i
}
}
return -1
}
if start >= count {
start = count - 1
}
for i := start; i >= 0; i-- {
if w := container.Child(i); isSelectable(w) {
return i
}
}
return -1
}
// focusExact applies an index change without additional traversal logic.
func (n *Navigator) focusExact(index int) bool {
container := n.currentContainer()
prev := container.Index()
container.SetIndex(index)
if container.Index() != prev {
n.notify(NavigatorEventFocusChanged)
}
return container.Index() == index
}
// isSelectable reports whether a widget participates in navigation.
func isSelectable(w Widget) bool {
if w == nil {
return false
}
if selectable, ok := w.(Selectable); ok {
if !selectable.CanSelect() {
return false
}
}
if enabler, ok := w.(EnableState); ok {
if !enabler.Enabled() {
return false
}
}
return true
}