1 package etk
2
3 import (
4 "image"
5 "image/color"
6 "math"
7 "sync"
8 "time"
9
10 "github.com/hajimehoshi/ebiten/v2"
11 )
12
13
14 type SelectionMode int
15
16
17 const (
18
19 SelectNone SelectionMode = iota
20
21
22 SelectRow
23
24
25 SelectColumn
26 )
27
28
29 type List struct {
30 rect image.Rectangle
31 grid *Grid
32 focused bool
33 itemHeight int
34 highlightColor color.RGBA
35 maxY int
36 selectionMode SelectionMode
37 selectedX, selectedY int
38 selectedTime time.Time
39 selectedFunc func(index int) (accept bool)
40 confirmedFunc func(index int)
41 items [][]Widget
42 offset int
43 recreateGrid bool
44 scrollRect image.Rectangle
45 scrollWidth int
46 scrollAreaColor color.RGBA
47 scrollHandleColor color.RGBA
48 scrollBorderSize int
49 scrollBorderTop color.RGBA
50 scrollBorderRight color.RGBA
51 scrollBorderBottom color.RGBA
52 scrollBorderLeft color.RGBA
53 scrollDrag bool
54 drawBorder bool
55 sync.Mutex
56 }
57
58 const (
59 initialPadding = 5
60 initialScrollWidth = 32
61 )
62
63 var (
64 initialForeground = color.RGBA{0, 0, 0, 255}
65 initialBackground = color.RGBA{255, 255, 255, 255}
66 initialScrollArea = color.RGBA{200, 200, 200, 255}
67 initialScrollHandle = color.RGBA{108, 108, 108, 255}
68 )
69
70
71 func NewList(itemHeight int, onSelected func(index int) (accept bool)) *List {
72 return &List{
73 grid: NewGrid(),
74 itemHeight: itemHeight,
75 highlightColor: color.RGBA{255, 255, 255, 255},
76 maxY: -1,
77 selectedY: -1,
78 selectedFunc: onSelected,
79 recreateGrid: true,
80 scrollWidth: initialScrollWidth,
81 scrollAreaColor: initialScrollArea,
82 scrollHandleColor: initialScrollHandle,
83 scrollBorderSize: Style.ScrollBorderSize,
84 scrollBorderTop: Style.ScrollBorderTop,
85 scrollBorderRight: Style.ScrollBorderRight,
86 scrollBorderBottom: Style.ScrollBorderBottom,
87 scrollBorderLeft: Style.ScrollBorderLeft,
88 }
89 }
90
91
92 func (l *List) Rect() image.Rectangle {
93 l.Lock()
94 defer l.Unlock()
95
96 return l.rect
97 }
98
99
100 func (l *List) SetRect(r image.Rectangle) {
101 l.Lock()
102 defer l.Unlock()
103
104 l.rect = r
105 if l.showScrollBar() {
106 r.Max.X -= l.scrollWidth
107 }
108 l.grid.SetRect(r)
109 l.selectionUpdated()
110 l.recreateGrid = true
111 }
112
113
114 func (l *List) Background() color.RGBA {
115 l.Lock()
116 defer l.Unlock()
117
118 return l.grid.Background()
119 }
120
121
122 func (l *List) SetBackground(background color.RGBA) {
123 l.Lock()
124 defer l.Unlock()
125
126 l.grid.SetBackground(background)
127 }
128
129
130 func (l *List) Focus() bool {
131 l.Lock()
132 defer l.Unlock()
133
134 return l.focused
135 }
136
137
138 func (l *List) SetFocus(focus bool) (accept bool) {
139 l.Lock()
140 defer l.Unlock()
141
142 l.focused = focus
143 return true
144 }
145
146
147 func (l *List) Visible() bool {
148 l.Lock()
149 defer l.Unlock()
150
151 return l.grid.Visible()
152 }
153
154
155 func (l *List) SetVisible(visible bool) {
156 l.Lock()
157 defer l.Unlock()
158
159 l.grid.SetVisible(visible)
160 }
161
162
163
164 func (l *List) SetColumnSizes(size ...int) {
165 l.Lock()
166 defer l.Unlock()
167
168 l.grid.SetColumnSizes(size...)
169 }
170
171
172 func (l *List) SetItemHeight(itemHeight int) {
173 l.Lock()
174 defer l.Unlock()
175
176 if l.itemHeight == itemHeight {
177 return
178 }
179 l.itemHeight = itemHeight
180
181 if l.maxY == -1 {
182 return
183 }
184 rowSizes := make([]int, l.maxY+1)
185 for i := range rowSizes {
186 rowSizes[i] = l.itemHeight
187 }
188 l.grid.SetRowSizes(rowSizes...)
189 }
190
191
192 func (l *List) SetSelectionMode(selectionMode SelectionMode) {
193 l.Lock()
194 defer l.Unlock()
195
196 if l.selectionMode == selectionMode {
197 return
198 }
199 l.selectionMode = selectionMode
200 }
201
202
203 func (l *List) SetHighlightColor(c color.RGBA) {
204 l.Lock()
205 defer l.Unlock()
206
207 l.highlightColor = c
208 }
209
210
211 func (l *List) SelectedItem() (x int, y int) {
212 l.Lock()
213 defer l.Unlock()
214
215 return l.selectedX, l.selectedY
216 }
217
218
219 func (l *List) SetSelectedItem(x int, y int) {
220 l.Lock()
221 defer l.Unlock()
222
223 l.selectedX, l.selectedY = x, y
224 l.selectionUpdated()
225 }
226
227
228 func (l *List) SetScrollBarWidth(width int) {
229 l.Lock()
230 defer l.Unlock()
231
232 if l.scrollWidth == width {
233 return
234 }
235
236 l.scrollWidth = width
237 }
238
239
240 func (l *List) SetScrollBarColors(area color.RGBA, handle color.RGBA) {
241 l.Lock()
242 defer l.Unlock()
243
244 l.scrollAreaColor, l.scrollHandleColor = area, handle
245 }
246
247
248 func (l *List) SetScrollBorderSize(size int) {
249 l.Lock()
250 defer l.Unlock()
251
252 l.scrollBorderSize = size
253 }
254
255
256
257 func (l *List) SetScrollBorderColors(top color.RGBA, right color.RGBA, bottom color.RGBA, left color.RGBA) {
258 l.Lock()
259 defer l.Unlock()
260
261 l.scrollBorderTop = top
262 l.scrollBorderRight = right
263 l.scrollBorderBottom = bottom
264 l.scrollBorderLeft = left
265 }
266
267
268
269
270 func (l *List) SetSelectedFunc(f func(index int) (accept bool)) {
271 l.Lock()
272 defer l.Unlock()
273
274 l.selectedFunc = f
275 }
276
277
278
279 func (l *List) SetConfirmedFunc(f func(index int)) {
280 l.Lock()
281 defer l.Unlock()
282
283 l.confirmedFunc = f
284 }
285
286
287
288
289 func (l *List) Children() []Widget {
290 l.Lock()
291 defer l.Unlock()
292
293 return l.grid.Children()
294 }
295
296
297 func (l *List) AddChildAt(w Widget, x int, y int) {
298 l.Lock()
299 defer l.Unlock()
300
301 for i := y; i >= len(l.items); i-- {
302 l.items = append(l.items, nil)
303 }
304 for i := x; i > len(l.items[y]); i-- {
305 l.items[y] = append(l.items[y], nil)
306 }
307 if l.selectionMode == SelectNone {
308 w = &WithoutMouseExceptScroll{Widget: w}
309 } else {
310 w = &WithoutMouse{Widget: w}
311 }
312 l.items[y] = append(l.items[y], w)
313 if y > l.maxY {
314 l.maxY = y
315 l.recreateGrid = true
316 }
317 }
318
319
320 func (l *List) Rows() int {
321 l.Lock()
322 defer l.Unlock()
323
324 return l.maxY + 1
325 }
326
327 func (l *List) showScrollBar() bool {
328 return len(l.items) > l.grid.rect.Dy()/l.itemHeight
329 }
330
331
332 func (l *List) clampOffset(offset int) int {
333 if offset >= len(l.items)-(l.grid.rect.Dy()/l.itemHeight) {
334 offset = len(l.items) - (l.grid.rect.Dy() / l.itemHeight)
335 }
336 if offset < 0 {
337 offset = 0
338 }
339 return offset
340 }
341
342 func (l *List) selectionUpdated() {
343 if l.selectedY < l.offset {
344 l.offset = l.selectedY
345 l.recreateGrid = true
346 return
347 }
348 visible := l.grid.rect.Dy()/l.itemHeight - 1
349 if visible < 1 {
350 visible = 1
351 }
352 if l.selectedY > l.offset+visible {
353 l.offset = l.selectedY - visible
354 l.recreateGrid = true
355 }
356 }
357
358
359
360 func (l *List) Cursor() ebiten.CursorShapeType {
361 return ebiten.CursorShapeDefault
362 }
363
364
365 func (l *List) HandleKeyboard(key ebiten.Key, r rune) (handled bool, err error) {
366 l.Lock()
367 defer l.Unlock()
368
369 if r == 0 {
370
371 for _, confirmKey := range Bindings.ConfirmKeyboard {
372 if key == confirmKey {
373 confirmedFunc := l.confirmedFunc
374 if confirmedFunc != nil {
375 l.Unlock()
376 confirmedFunc(l.selectedY)
377 l.Lock()
378 }
379 return true, nil
380 }
381 }
382
383
384 move := func(x int, y int) {
385 y = l.selectedY + y
386 if y >= 0 && y <= l.maxY {
387 l.selectedY = y
388 l.selectionUpdated()
389 }
390 }
391 for _, leftKey := range Bindings.MoveLeftKeyboard {
392 if key == leftKey {
393 move(-1, 0)
394 return true, nil
395 }
396 }
397 for _, rightKey := range Bindings.MoveRightKeyboard {
398 if key == rightKey {
399 move(1, 0)
400 return true, nil
401 }
402 }
403 for _, downKey := range Bindings.MoveDownKeyboard {
404 if key == downKey {
405 move(0, 1)
406 return true, nil
407 }
408 }
409 for _, upKey := range Bindings.MoveUpKeyboard {
410 if key == upKey {
411 move(0, -1)
412 return true, nil
413 }
414 }
415 }
416
417 return l.grid.HandleKeyboard(key, r)
418 }
419
420
421 func (l *List) SetDrawBorder(drawBorder bool) {
422 l.drawBorder = drawBorder
423 }
424
425
426
427 func (l *List) HandleMouse(cursor image.Point, pressed bool, clicked bool) (handled bool, err error) {
428 l.Lock()
429 defer l.Unlock()
430
431 _, scroll := ebiten.Wheel()
432 if scroll != 0 {
433 offset := l.clampOffset(l.offset - int(math.Round(scroll)))
434 if offset != l.offset {
435 l.offset = offset
436 l.recreateGrid = true
437 }
438 }
439
440 if l.showScrollBar() && (pressed || l.scrollDrag) {
441 if pressed && cursor.In(l.scrollRect) {
442 dragY := cursor.Y - l.grid.rect.Min.Y
443 if dragY < 0 {
444 dragY = 0
445 } else if dragY > l.scrollRect.Dy() {
446 dragY = l.scrollRect.Dy()
447 }
448
449 pct := float64(dragY) / float64(l.scrollRect.Dy())
450 if pct < 0 {
451 pct = 0
452 } else if pct > 1 {
453 pct = 1
454 }
455
456 lastOffset := l.offset
457 offset := l.clampOffset(int(math.Round(float64(len(l.items)-(l.grid.rect.Dy()/l.itemHeight)) * pct)))
458 if offset != lastOffset {
459 l.offset = offset
460 l.recreateGrid = true
461 }
462 l.scrollDrag = true
463 return true, nil
464 } else if !pressed {
465 l.scrollDrag = false
466 }
467 }
468
469 if !clicked || (cursor.X == 0 && cursor.Y == 0) {
470 return true, nil
471 }
472 selected := l.offset + (cursor.Y-l.grid.rect.Min.Y)/l.itemHeight
473 if selected >= 0 && selected <= l.maxY {
474 lastSelected := l.selectedY
475 l.selectedY = selected
476
477 selectedFunc := l.selectedFunc
478 if selectedFunc != nil {
479 l.Unlock()
480 accept := selectedFunc(l.selectedY)
481 l.Lock()
482 if !accept {
483 l.selectedY = lastSelected
484 return true, nil
485 }
486 }
487
488 l.selectionUpdated()
489
490 if selected == lastSelected && time.Since(l.selectedTime) <= Bindings.DoubleClickThreshold {
491 confirmedFunc := l.confirmedFunc
492 if confirmedFunc != nil {
493 l.Unlock()
494 confirmedFunc(l.selectedY)
495 l.Lock()
496 }
497 l.selectedTime = time.Time{}
498 return true, nil
499 }
500
501 l.selectedTime = time.Now()
502 }
503 return true, nil
504 }
505
506
507 func (l *List) Draw(screen *ebiten.Image) error {
508 l.Lock()
509 defer l.Unlock()
510
511 if l.recreateGrid {
512 maxY := l.grid.rect.Dy()/l.itemHeight + 1
513 l.offset = l.clampOffset(l.offset)
514 l.grid.Clear()
515 rowSizes := make([]int, l.maxY+1)
516 for i := range rowSizes {
517 rowSizes[i] = l.itemHeight
518 }
519 l.grid.SetRowSizes(rowSizes...)
520 var y int
521 for i := range l.items {
522 if i < l.offset {
523 continue
524 } else if y >= maxY {
525 break
526 }
527 for x := range l.items[i] {
528 w := l.items[i][x]
529 if w == nil {
530 continue
531 }
532 l.grid.AddChildAt(w, x, y, 1, 1)
533 }
534 y++
535 }
536 r := l.rect
537 if l.showScrollBar() {
538 r.Max.X -= l.scrollWidth
539 }
540 l.grid.SetRect(r)
541 l.recreateGrid = false
542 }
543
544
545 err := l.grid.Draw(screen.SubImage(l.rect).(*ebiten.Image))
546 if err != nil {
547 return err
548 }
549
550
551 drawHighlight := l.selectionMode != SelectNone && l.selectedY >= 0
552 if drawHighlight {
553 x, y := l.grid.rect.Min.X, l.grid.rect.Min.Y+(l.selectedY-l.offset)*l.itemHeight
554 w, h := l.grid.rect.Dx(), l.itemHeight
555 r := clampRect(image.Rect(x, y, x+w, y+h), l.rect)
556 if r.Dx() > 0 && r.Dy() > 0 {
557 screen.SubImage(r).(*ebiten.Image).Fill(l.highlightColor)
558 }
559 }
560
561
562 if l.drawBorder {
563 const borderSize = 4
564 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.ButtonBorderBottom)
565 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.ButtonBorderBottom)
566 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.ButtonBorderBottom)
567 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.ButtonBorderBottom)
568 }
569
570
571 if !l.showScrollBar() {
572 return nil
573 }
574 w, h := l.rect.Dx(), l.rect.Dy()
575 scrollAreaX, scrollAreaY := l.rect.Min.X+w-l.scrollWidth, l.rect.Min.Y
576 l.scrollRect = image.Rect(scrollAreaX, scrollAreaY, scrollAreaX+l.scrollWidth, scrollAreaY+h)
577
578 scrollBarH := l.scrollWidth / 2
579 if scrollBarH < 4 {
580 scrollBarH = 4
581 }
582
583 scrollX, scrollY := l.rect.Min.X+w-l.scrollWidth, l.rect.Min.Y
584 pct := float64(-l.offset) / float64(len(l.items)-(l.rect.Dy()/l.itemHeight))
585 scrollY -= int(float64(h-scrollBarH) * pct)
586 scrollBarRect := image.Rect(scrollX, scrollY, scrollX+l.scrollWidth, scrollY+scrollBarH)
587
588 screen.SubImage(l.scrollRect).(*ebiten.Image).Fill(l.scrollAreaColor)
589 screen.SubImage(scrollBarRect).(*ebiten.Image).Fill(l.scrollHandleColor)
590
591
592 if l.scrollBorderSize != 0 {
593 r := scrollBarRect
594 screen.SubImage(image.Rect(r.Min.X, r.Min.Y, r.Min.X+l.scrollBorderSize, r.Max.Y)).(*ebiten.Image).Fill(l.scrollBorderLeft)
595 screen.SubImage(image.Rect(r.Min.X, r.Min.Y, r.Max.X, r.Min.Y+l.scrollBorderSize)).(*ebiten.Image).Fill(l.scrollBorderTop)
596 screen.SubImage(image.Rect(r.Max.X-l.scrollBorderSize, r.Min.Y, r.Max.X, r.Max.Y)).(*ebiten.Image).Fill(l.scrollBorderRight)
597 screen.SubImage(image.Rect(r.Min.X, r.Max.Y-l.scrollBorderSize, r.Max.X, r.Max.Y)).(*ebiten.Image).Fill(l.scrollBorderBottom)
598 }
599 return nil
600 }
601
602
603 func (l *List) Clear() {
604 l.Lock()
605 defer l.Unlock()
606
607 l.items = nil
608 l.maxY = -1
609 l.selectedX, l.selectedY = 0, -1
610 l.offset = 0
611 l.recreateGrid = true
612 }
613
View as plain text