1 package etk
2
3 import (
4 "image"
5 "image/color"
6 "math"
7 "sync"
8
9 "github.com/hajimehoshi/ebiten/v2"
10 )
11
12
13 type SelectionMode int
14
15
16 const (
17
18 SelectNone SelectionMode = iota
19
20
21 SelectRow
22
23
24 SelectColumn
25 )
26
27
28 type List struct {
29 grid *Grid
30 itemHeight int
31 highlightColor color.RGBA
32 maxY int
33 selectionMode SelectionMode
34 selectedX, selectedY int
35 selectedFunc func(index int) (accept bool)
36 items [][]Widget
37 offset int
38 recreateGrid bool
39 scrollRect image.Rectangle
40 scrollWidth int
41 scrollAreaColor color.RGBA
42 scrollHandleColor color.RGBA
43 scrollDrag bool
44 drawBorder bool
45 sync.Mutex
46 }
47
48 const (
49 initialPadding = 5
50 initialScrollWidth = 32
51 )
52
53 var (
54 initialForeground = color.RGBA{0, 0, 0, 255}
55 initialBackground = color.RGBA{255, 255, 255, 255}
56 initialScrollArea = color.RGBA{200, 200, 200, 255}
57 initialScrollHandle = color.RGBA{108, 108, 108, 255}
58 )
59
60
61 func NewList(itemHeight int, onSelected func(index int) (accept bool)) *List {
62 return &List{
63 grid: NewGrid(),
64 itemHeight: itemHeight,
65 highlightColor: color.RGBA{255, 255, 255, 255},
66 maxY: -1,
67 selectedY: -1,
68 selectedFunc: onSelected,
69 recreateGrid: true,
70 scrollWidth: initialScrollWidth,
71 scrollAreaColor: initialScrollArea,
72 scrollHandleColor: initialScrollHandle,
73 }
74 }
75
76
77 func (l *List) Rect() image.Rectangle {
78 l.Lock()
79 defer l.Unlock()
80
81 return l.grid.Rect()
82 }
83
84
85 func (l *List) SetRect(r image.Rectangle) {
86 l.Lock()
87 defer l.Unlock()
88
89 l.grid.SetRect(r)
90 l.recreateGrid = true
91 }
92
93
94 func (l *List) Background() color.RGBA {
95 l.Lock()
96 defer l.Unlock()
97
98 return l.grid.Background()
99 }
100
101
102 func (l *List) SetBackground(background color.RGBA) {
103 l.Lock()
104 defer l.Unlock()
105
106 l.grid.SetBackground(background)
107 }
108
109
110 func (l *List) Focus() bool {
111 l.Lock()
112 defer l.Unlock()
113
114 return l.grid.Focus()
115 }
116
117
118 func (l *List) SetFocus(focus bool) (accept bool) {
119 l.Lock()
120 defer l.Unlock()
121
122 return l.grid.SetFocus(focus)
123 }
124
125
126 func (l *List) Visible() bool {
127 l.Lock()
128 defer l.Unlock()
129
130 return l.grid.Visible()
131 }
132
133
134 func (l *List) SetVisible(visible bool) {
135 l.Lock()
136 defer l.Unlock()
137
138 l.grid.SetVisible(visible)
139 }
140
141
142
143 func (l *List) SetColumnSizes(size ...int) {
144 l.Lock()
145 defer l.Unlock()
146
147 l.grid.SetColumnSizes(size...)
148 }
149
150
151 func (l *List) SetItemHeight(itemHeight int) {
152 l.Lock()
153 defer l.Unlock()
154
155 if l.itemHeight == itemHeight {
156 return
157 }
158 l.itemHeight = itemHeight
159
160 if l.maxY == -1 {
161 return
162 }
163 rowSizes := make([]int, l.maxY+1)
164 for i := range rowSizes {
165 rowSizes[i] = l.itemHeight
166 }
167 l.grid.SetRowSizes(rowSizes...)
168 }
169
170
171 func (l *List) SetSelectionMode(selectionMode SelectionMode) {
172 l.Lock()
173 defer l.Unlock()
174
175 if l.selectionMode == selectionMode {
176 return
177 }
178 l.selectionMode = selectionMode
179 }
180
181
182 func (l *List) SetHighlightColor(c color.RGBA) {
183 l.Lock()
184 defer l.Unlock()
185
186 l.highlightColor = c
187 }
188
189
190 func (l *List) SelectedItem() (x int, y int) {
191 l.Lock()
192 defer l.Unlock()
193
194 return l.selectedX, l.selectedY
195 }
196
197
198 func (l *List) SetSelectedItem(x int, y int) {
199 l.Lock()
200 defer l.Unlock()
201
202 l.selectedX, l.selectedY = x, y
203 }
204
205
206 func (l *List) SetScrollBarWidth(width int) {
207 l.Lock()
208 defer l.Unlock()
209
210 if l.scrollWidth == width {
211 return
212 }
213
214 l.scrollWidth = width
215 }
216
217
218 func (l *List) SetScrollBarColors(area color.RGBA, handle color.RGBA) {
219 l.Lock()
220 defer l.Unlock()
221
222 l.scrollAreaColor, l.scrollHandleColor = area, handle
223 }
224
225
226
227
228 func (l *List) SetSelectedFunc(f func(index int) (accept bool)) {
229 l.Lock()
230 defer l.Unlock()
231
232 l.selectedFunc = f
233 }
234
235
236
237
238 func (l *List) Children() []Widget {
239 l.Lock()
240 defer l.Unlock()
241
242 return l.grid.Children()
243 }
244
245
246 func (l *List) AddChildAt(w Widget, x int, y int) {
247 l.Lock()
248 defer l.Unlock()
249
250 for i := y; i >= len(l.items); i-- {
251 l.items = append(l.items, nil)
252 }
253 for i := x; i > len(l.items[y]); i-- {
254 l.items[y] = append(l.items[y], nil)
255 }
256 l.items[y] = append(l.items[y], &ignoreMouse{w})
257 if y > l.maxY {
258 l.maxY = y
259 l.recreateGrid = true
260 }
261 }
262
263
264 func (l *List) Rows() int {
265 l.Lock()
266 defer l.Unlock()
267
268 return l.maxY + 1
269 }
270
271 func (l *List) showScrollBar() bool {
272 return len(l.items) > l.grid.rect.Dy()/l.itemHeight
273 }
274
275
276 func (l *List) clampOffset(offset int) int {
277 if offset >= len(l.items)-(l.grid.rect.Dy()/l.itemHeight) {
278 offset = len(l.items) - (l.grid.rect.Dy() / l.itemHeight)
279 }
280 if offset < 0 {
281 offset = 0
282 }
283 return offset
284 }
285
286
287 func (l *List) HandleKeyboard(key ebiten.Key, r rune) (handled bool, err error) {
288 l.Lock()
289 defer l.Unlock()
290
291 return l.grid.HandleKeyboard(key, r)
292 }
293
294
295 func (l *List) SetDrawBorder(drawBorder bool) {
296 l.drawBorder = drawBorder
297 }
298
299
300
301 func (l *List) HandleMouse(cursor image.Point, pressed bool, clicked bool) (handled bool, err error) {
302 l.Lock()
303 defer l.Unlock()
304
305 _, scroll := ebiten.Wheel()
306 if scroll != 0 {
307 offset := l.clampOffset(l.offset - int(math.Round(scroll)))
308 if offset != l.offset {
309 l.offset = offset
310 l.recreateGrid = true
311 }
312 }
313
314 if l.showScrollBar() && (pressed || l.scrollDrag) {
315 if pressed && cursor.In(l.scrollRect) {
316 dragY := cursor.Y - l.grid.rect.Min.Y
317 if dragY < 0 {
318 dragY = 0
319 } else if dragY > l.scrollRect.Dy() {
320 dragY = l.scrollRect.Dy()
321 }
322
323 pct := float64(dragY) / float64(l.scrollRect.Dy())
324 if pct < 0 {
325 pct = 0
326 } else if pct > 1 {
327 pct = 1
328 }
329
330 lastOffset := l.offset
331 offset := l.clampOffset(int(math.Round(float64(len(l.items)-(l.grid.rect.Dy()/l.itemHeight)) * pct)))
332 if offset != lastOffset {
333 l.offset = offset
334 l.recreateGrid = true
335 }
336 l.scrollDrag = true
337 return true, nil
338 } else if !pressed {
339 l.scrollDrag = false
340 }
341 }
342
343 if !clicked || (cursor.X == 0 && cursor.Y == 0) {
344 return true, nil
345 }
346 selected := (cursor.Y - l.grid.rect.Min.Y) / l.itemHeight
347 if selected >= 0 && selected <= l.maxY {
348 lastSelected := l.selectedY
349 l.selectedY = selected
350 if l.selectedFunc != nil {
351 accept := l.selectedFunc(l.selectedY)
352 if !accept {
353 l.selectedY = lastSelected
354 }
355 }
356 }
357 return true, nil
358 }
359
360
361 func (l *List) Draw(screen *ebiten.Image) error {
362 l.Lock()
363 defer l.Unlock()
364
365 if l.recreateGrid {
366 maxY := l.grid.rect.Dy() / l.itemHeight
367 l.offset = l.clampOffset(l.offset)
368 l.grid.Clear()
369 rowSizes := make([]int, l.maxY+1)
370 for i := range rowSizes {
371 rowSizes[i] = l.itemHeight
372 }
373 l.grid.SetRowSizes(rowSizes...)
374 var y int
375 for i := range l.items {
376 if i < l.offset {
377 continue
378 } else if y >= maxY {
379 break
380 }
381 for x := range l.items[i] {
382 w := l.items[i][x]
383 if w == nil {
384 continue
385 }
386 l.grid.AddChildAt(w, x, y, 1, 1)
387 }
388 y++
389 }
390 l.recreateGrid = false
391 }
392
393
394 err := l.grid.Draw(screen)
395 if err != nil {
396 return err
397 }
398
399
400 drawHighlight := l.selectionMode != SelectNone && l.selectedY >= 0
401 if drawHighlight {
402 {
403 x, y := l.grid.rect.Min.X, l.grid.rect.Min.Y+l.selectedY*l.itemHeight
404 w, h := l.grid.rect.Dx(), l.itemHeight
405 r := image.Rect(x, y, x+w, y+h)
406 screen.SubImage(r).(*ebiten.Image).Fill(l.highlightColor)
407 }
408 }
409
410
411 if l.drawBorder {
412 const borderSize = 4
413 screen.SubImage(image.Rect(l.grid.rect.Min.X, l.grid.rect.Min.Y, l.grid.rect.Max.X, l.grid.rect.Min.Y+borderSize)).(*ebiten.Image).Fill(Style.BorderColorBottom)
414 screen.SubImage(image.Rect(l.grid.rect.Min.X, l.grid.rect.Max.Y-borderSize, l.grid.rect.Max.X, l.grid.rect.Max.Y)).(*ebiten.Image).Fill(Style.BorderColorBottom)
415 screen.SubImage(image.Rect(l.grid.rect.Min.X, l.grid.rect.Min.Y, l.grid.rect.Min.X+borderSize, l.grid.rect.Max.Y)).(*ebiten.Image).Fill(Style.BorderColorBottom)
416 screen.SubImage(image.Rect(l.grid.rect.Max.X-borderSize, l.grid.rect.Min.Y, l.grid.rect.Max.X, l.grid.rect.Max.Y)).(*ebiten.Image).Fill(Style.BorderColorBottom)
417 }
418
419
420 if !l.showScrollBar() {
421 return nil
422 }
423 w, h := l.grid.rect.Dx(), l.grid.rect.Dy()
424 scrollAreaX, scrollAreaY := l.grid.rect.Min.X+w-l.scrollWidth, l.grid.rect.Min.Y
425 l.scrollRect = image.Rect(scrollAreaX, scrollAreaY, scrollAreaX+l.scrollWidth, scrollAreaY+h)
426
427 scrollBarH := l.scrollWidth / 2
428 if scrollBarH < 4 {
429 scrollBarH = 4
430 }
431
432 scrollX, scrollY := l.grid.rect.Min.X+w-l.scrollWidth, l.grid.rect.Min.Y
433 pct := float64(-l.offset) / float64(len(l.items)-(l.grid.rect.Dy()/l.itemHeight))
434 scrollY -= int(float64(h-scrollBarH) * pct)
435 scrollBarRect := image.Rect(scrollX, scrollY, scrollX+l.scrollWidth, scrollY+scrollBarH)
436
437 screen.SubImage(l.scrollRect).(*ebiten.Image).Fill(l.scrollAreaColor)
438 screen.SubImage(scrollBarRect).(*ebiten.Image).Fill(l.scrollHandleColor)
439 return nil
440 }
441
442
443 func (l *List) Clear() {
444 l.Lock()
445 defer l.Unlock()
446
447 l.items = nil
448 l.maxY = -1
449 l.selectedX, l.selectedY = 0, -1
450 l.offset = 0
451 l.recreateGrid = true
452 }
453
View as plain text