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