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