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
|
package ui
import (
"github.com/gdamore/tcell/v2"
"rickub.com/turbo-editors/turbo-core/theme"
)
// ListBox is a scrolling list of strings with one selected line.
//
// It is what the Open File dialog, the theme picker and the window list are
// all made of, so it carries the scrolling and the keyboard handling those
// three would otherwise each repeat.
type ListBox struct {
FocusBox
items []string
selected int
top int
// OnChoose is called when a line is picked with Enter or a double click.
OnChoose func(index int)
// OnSelect is called whenever the highlighted line changes.
OnSelect func(index int)
}
// NewListBox returns a list showing items, with the first one selected.
func NewListBox(items []string) *ListBox {
return &ListBox{items: items}
}
// Items returns the lines the list holds.
func (l *ListBox) Items() []string { return l.items }
// SetItems replaces the lines and selects the first one.
func (l *ListBox) SetItems(items []string) {
l.items = items
l.top = 0
l.setSelected(0)
}
// Selected returns the index of the highlighted line, or -1 when the list is
// empty.
func (l *ListBox) Selected() int {
if len(l.items) == 0 {
return -1
}
return l.selected
}
// SelectedItem returns the highlighted line, or the empty string when the list
// is empty.
func (l *ListBox) SelectedItem() string {
if index := l.Selected(); index >= 0 {
return l.items[index]
}
return ""
}
// Select highlights a line, clamping the index into the list.
func (l *ListBox) Select(index int) { l.setSelected(index) }
// setSelected moves the highlight and tells the owner, if it changed.
func (l *ListBox) setSelected(index int) {
if len(l.items) == 0 {
l.selected = 0
return
}
index = min(max(index, 0), len(l.items)-1)
if index == l.selected {
return
}
l.selected = index
if l.OnSelect != nil {
l.OnSelect(index)
}
}
// Choose runs the OnChoose action for the highlighted line.
func (l *ListBox) Choose() {
if l.OnChoose != nil && l.Selected() >= 0 {
l.OnChoose(l.selected)
}
}
// visibleHeight is how many lines fit, one column being kept for the scroll
// bar down the right-hand edge.
func (l *ListBox) visibleHeight() int { return l.Bounds().H }
// scrollToSelection slides the view so the highlighted line is inside it.
func (l *ListBox) scrollToSelection() {
height := l.visibleHeight()
if height <= 0 {
return
}
l.top = min(l.top, l.selected)
if l.selected >= l.top+height {
l.top = l.selected - height + 1
}
l.top = min(max(l.top, 0), max(len(l.items)-height, 0))
}
// Draw paints the visible lines and the scroll bar.
func (l *ListBox) Draw(p *Painter, th *theme.Theme) {
area := p.Sub(l.Bounds())
normal := th.Style(theme.KeyList)
area.Clear(normal)
l.scrollToSelection()
width := max(area.Size().W-1, 0)
for row := range area.Size().H {
index := l.top + row
if index >= len(l.items) {
break
}
style := normal
if index == l.selected {
style = l.selectionStyle(th)
area.HLine(0, row, width, ' ', style)
}
area.TextLimited(0, row, width, l.items[index], style)
}
DrawVScrollBar(area, area.Size().W-1, 0, area.Size().H,
l.top, area.Size().H, len(l.items),
th.Style(theme.KeyScrollBar), th.Style(theme.KeyScrollBarThumb))
}
// selectionStyle is the highlight, dimmed when the list does not have the
// focus so that a dialog shows where the keyboard actually is.
func (l *ListBox) selectionStyle(th *theme.Theme) tcell.Style {
if l.Focused() {
return th.Style(theme.KeyListSelected)
}
return th.Style(theme.KeyListUnfocused)
}
// HandleKey walks the list.
func (l *ListBox) HandleKey(ev *tcell.EventKey) bool {
if !l.Focused() {
return false
}
height := max(l.visibleHeight(), 1)
switch ev.Key() {
case tcell.KeyUp:
l.setSelected(l.selected - 1)
case tcell.KeyDown:
l.setSelected(l.selected + 1)
case tcell.KeyPgUp:
l.setSelected(l.selected - height)
case tcell.KeyPgDn:
l.setSelected(l.selected + height)
case tcell.KeyHome:
l.setSelected(0)
case tcell.KeyEnd:
l.setSelected(len(l.items) - 1)
case tcell.KeyEnter:
l.Choose()
default:
return false
}
return true
}
// HandleMouse selects the clicked line, and chooses it on a second click or a
// wheel-free double click.
func (l *ListBox) HandleMouse(ev *tcell.EventMouse) bool {
if !l.hit(ev) {
return false
}
switch ev.Buttons() {
case tcell.WheelUp:
l.setSelected(l.selected - 1)
case tcell.WheelDown:
l.setSelected(l.selected + 1)
case tcell.Button1:
_, y := ev.Position()
index := l.top + y - l.Bounds().Y
if index == l.selected {
l.Choose() // clicking the already-selected line opens it
}
l.setSelected(index)
}
return true
}
|