-
Notifications
You must be signed in to change notification settings - Fork 1
/
listbox.go
309 lines (272 loc) · 7.73 KB
/
listbox.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
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
package ui
import (
"fmt"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui/style"
)
// ListBox is a selectable list of values like a multi-line SelectBox.
type ListBox struct {
*Frame
name string
children []*ListValue
style *style.ListBox
supervisor *Supervisor
list *Frame
scrollbar *ScrollBar
scrollFraction float64
maxHeight int
// Variable bindings: give these pointers to your values.
Variable interface{} // pointer to e.g. a string or int
// TextVariable *string // string value
// IntVariable *int // integer value
}
// ListValue is an item in the ListBox. It has an arbitrary widget as a
// "label" (usually a Label) and a value (string or int) when it's "selected"
type ListValue struct {
Frame *Frame
Label Widget
Value interface{}
}
// NewListBox creates a new ListBox.
func NewListBox(name string, config ListBox) *ListBox {
w := &ListBox{
Frame: NewFrame(name + " Frame"),
list: NewFrame(name + " List"),
name: name,
children: []*ListValue{},
Variable: config.Variable,
// TextVariable: config.TextVariable,
// IntVariable: config.IntVariable,
style: &style.DefaultListBox,
}
// if config.Width > 0 && config.Height > 0 {
// w.Frame.Resize(render.NewRect(config.Width, config.Height))
// }
w.IDFunc(func() string {
return fmt.Sprintf("ListBox<%s>", name)
})
w.SetStyle(Theme.ListBox)
w.setup()
return w
}
// SetStyle sets the listbox style.
func (w *ListBox) SetStyle(v *style.ListBox) {
if v == nil {
v = &style.DefaultListBox
}
w.style = v
fmt.Printf("set style: %+v\n", v)
w.Frame.Configure(Config{
BorderSize: w.style.BorderSize,
BorderStyle: BorderStyle(w.style.BorderStyle),
Background: w.style.Background,
})
// If the child is a Label, apply the foreground color.
// if label, ok := w.child.(*Label); ok {
// label.Font.Color = w.style.Foreground
// }
}
// GetStyle gets the listbox style.
func (w *ListBox) GetStyle() *style.ListBox {
return w.style
}
// Supervise the ListBox. This is necessary for granting mouse-over events
// to the items in the list.
func (w *ListBox) Supervise(s *Supervisor) {
w.supervisor = s
w.scrollbar.Supervise(s)
// Add all the list items to be supervised.
for _, c := range w.children {
w.supervisor.Add(c.Frame)
}
}
// AddLabel adds a simple text-based label to the Listbox.
// The label is the text value to display.
// The value is the underlying value (string or int) for the TextVariable or IntVariable.
// The function callback runs when the option is picked.
func (w *ListBox) AddLabel(label string, value interface{}, f func()) {
row := NewFrame(label + " Frame")
child := NewLabel(Label{
Text: label,
Font: render.Text{
Color: w.style.Foreground,
Size: 11,
Padding: 2,
},
})
row.Pack(child, Pack{
Side: W,
FillX: true,
})
// Add this label and its value mapping to the ListBox.
w.children = append(w.children, &ListValue{
Frame: row,
Label: child,
Value: value,
})
// Event handlers for the item row.
// row.Handle(MouseOver, func(ed EventData) error {
// if ed.Point.Inside(AbsoluteRect(w.scrollbar)) {
// return nil // ignore if over scrollbar
// }
// row.SetBackground(w.style.HoverBackground)
// child.Font.Color = w.style.HoverForeground
// return nil
// })
row.Handle(MouseMove, func(ed EventData) error {
if ed.Point.Inside(AbsoluteRect(w.scrollbar)) {
// we wandered onto the scrollbar, cancel mouseover
return row.Event(MouseOut, ed)
}
row.SetBackground(w.style.HoverBackground)
child.Font.Color = w.style.HoverForeground
return nil
})
row.Handle(MouseOut, func(ed EventData) error {
if cur, ok := w.GetValue(); ok && cur == value {
row.SetBackground(w.style.SelectedBackground)
child.Font.Color = w.style.SelectedForeground
} else {
fmt.Printf("couldn't get value? %+v %+v\n", cur, ok)
row.SetBackground(w.style.Background)
child.Font.Color = w.style.Foreground
}
return nil
})
row.Handle(MouseUp, func(ed EventData) error {
if cur, ok := w.GetValue(); ok && cur == value {
row.SetBackground(w.style.SelectedBackground)
child.Font.Color = w.style.SelectedForeground
} else {
row.SetBackground(w.style.Background)
child.Font.Color = w.style.Foreground
}
return nil
})
row.Handle(Click, func(ed EventData) error {
// Trigger if we are not hovering over the (overlapping) scrollbar.
if !ed.Point.Inside(AbsoluteRect(w.scrollbar)) {
w.Event(Change, EventData{
Supervisor: w.supervisor,
Value: value,
})
}
return nil
})
// Append the item into the ListBox frame.
w.Frame.Pack(row, Pack{
Side: N,
PadY: 1,
Fill: true,
})
// If the current text label isn't in the options, pick
// the first option.
if _, ok := w.GetValue(); !ok {
w.Variable = w.children[0].Value
row.SetBackground(w.style.SelectedBackground)
}
}
// TODO: RemoveItem()
// GetValue returns the currently selected item in the ListBox.
//
// Returns the SelectValue and true on success, and the Label or underlying Value
// can be read from the SelectValue struct. If no valid option is selected, the
// bool value returns false.
func (w *ListBox) GetValue() (*ListValue, bool) {
for _, row := range w.children {
if w.Variable != nil && w.Variable == row.Value {
return row, true
}
}
return nil, false
}
// SetValueByLabel sets the currently selected option to the given label.
func (w *ListBox) SetValueByLabel(label string) bool {
for _, option := range w.children {
if child, ok := option.Label.(*Label); ok && child.Text == label {
w.Variable = option.Value
return true
}
}
return false
}
// SetValue sets the currently selected option to the given value.
func (w *ListBox) SetValue(value interface{}) bool {
w.Variable = value
for _, option := range w.children {
if option.Value == value {
w.Variable = option.Value
return true
}
}
return false
}
// Compute to re-evaluate the button state (in the case of radio buttons where
// a different button will affect the state of this one when clicked).
func (w *ListBox) Compute(e render.Engine) {
w.computeVisible()
w.Frame.Compute(e)
}
// setup the UI components and event handlers.
func (w *ListBox) setup() {
// w.Configure(Config{
// BorderSize: 1,
// BorderStyle: BorderSunken,
// Background: theme.InputBackgroundColor,
// })
w.scrollbar = NewScrollBar(ScrollBar{})
w.scrollbar.Handle(Scroll, func(ed EventData) error {
fmt.Printf("Scroll event: %f%% unit %d\n", ed.ScrollFraction*100, ed.ScrollUnits)
w.scrollFraction = ed.ScrollFraction
return nil
})
w.Frame.Pack(w.scrollbar, Pack{
Side: E,
FillY: true,
Padding: 0,
})
// w.Frame.Pack(w.list, Pack{
// Side: E,
// FillY: true,
// Expand: true,
// })
}
// Compute which items of the list should be visible based on scroll position.
func (w *ListBox) computeVisible() {
if len(w.children) == 0 {
return
}
// Sample the first element's height.
var (
myHeight = w.height
maxTop = w.maxHeight - myHeight + w.children[len(w.children)-1].Frame.height
top = int(w.scrollFraction * float64(maxTop))
// itemHeight = w.children[0].Label.Size().H
)
var (
scan int
scrollFreed int
totalHeight int
)
for _, c := range w.children {
childHeight := c.Frame.Size().H + 2
if top > 0 && scan+childHeight < top {
scrollFreed += childHeight
c.Frame.Hide()
} else if scan+childHeight > myHeight+scrollFreed {
c.Frame.Hide()
} else {
c.Frame.Show()
}
scan += childHeight // for padding
totalHeight += childHeight
}
w.maxHeight = totalHeight
}
func (w *ListBox) Present(e render.Engine, p render.Point) {
w.Frame.Present(e, p)
// HACK to get the scrollbar to appear on top of the list frame :(
pos := AbsolutePosition(w.scrollbar)
// pos.X += w.BoxThickness(w.style.BorderSize / 2) // HACK
w.scrollbar.Present(e, pos)
}