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