...

Source file src/code.rocket9labs.com/tslocum/etk/list.go

Documentation: code.rocket9labs.com/tslocum/etk

     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  // SelectionMode represents a mode of selection.
    14  type SelectionMode int
    15  
    16  // Selection modes.
    17  const (
    18  	// SelectNone disables selection.
    19  	SelectNone SelectionMode = iota
    20  
    21  	// SelectRow enables selection by row.
    22  	SelectRow
    23  
    24  	// SelectColumn enables selection by column.
    25  	SelectColumn
    26  )
    27  
    28  // List is a list of widgets.
    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  // NewList returns a new List widget.
    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  // Rect returns the position and size of the widget.
    94  func (l *List) Rect() image.Rectangle {
    95  	l.Lock()
    96  	defer l.Unlock()
    97  
    98  	return l.rect
    99  }
   100  
   101  // SetRect sets the position and size of the widget.
   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  // Background returns the background color of the widget.
   116  func (l *List) Background() color.RGBA {
   117  	l.Lock()
   118  	defer l.Unlock()
   119  
   120  	return l.grid.Background()
   121  }
   122  
   123  // SetBackground sets the background color of the widget.
   124  func (l *List) SetBackground(background color.RGBA) {
   125  	l.Lock()
   126  	defer l.Unlock()
   127  
   128  	l.grid.SetBackground(background)
   129  }
   130  
   131  // Focus returns the focus state of the widget.
   132  func (l *List) Focus() bool {
   133  	l.Lock()
   134  	defer l.Unlock()
   135  
   136  	return l.focused
   137  }
   138  
   139  // SetFocus sets the focus state of the widget.
   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  // Visible returns the visibility of the widget.
   149  func (l *List) Visible() bool {
   150  	l.Lock()
   151  	defer l.Unlock()
   152  
   153  	return l.grid.Visible()
   154  }
   155  
   156  // SetVisible sets the visibility of the widget.
   157  func (l *List) SetVisible(visible bool) {
   158  	l.Lock()
   159  	defer l.Unlock()
   160  
   161  	l.grid.SetVisible(visible)
   162  }
   163  
   164  // SetColumnSizes sets the size of each column. A size of -1 represents an equal
   165  // proportion of the available space.
   166  func (l *List) SetColumnSizes(size ...int) {
   167  	l.Lock()
   168  	defer l.Unlock()
   169  
   170  	l.grid.SetColumnSizes(size...)
   171  }
   172  
   173  // SetItemHeight sets the height of the list items.
   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  // SetSelectionMode sets the selection mode of the list.
   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  // SetHighlightColor sets the color used to highlight the currently selected item.
   205  func (l *List) SetHighlightColor(c color.RGBA) {
   206  	l.Lock()
   207  	defer l.Unlock()
   208  
   209  	l.highlightColor = c
   210  }
   211  
   212  // SelectedItem returns the selected list item.
   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  // SetSelectedItem sets the selected list item.
   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  // SetScrollBarWidth sets the width of the scroll bar.
   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  // SetScrollBarColors sets the color of the scroll bar area and handle.
   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  // SetScrollBorderSize sets the size of the border around the scroll bar handle.
   250  func (l *List) SetScrollBorderSize(size int) {
   251  	l.Lock()
   252  	defer l.Unlock()
   253  
   254  	l.scrollBorderSize = size
   255  }
   256  
   257  // SetScrollBorderColor sets the color of the top, right, bottom and left border
   258  // of the scroll bar handle.
   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  // SetSelectedFunc sets a handler which is called when a list item is selected.
   270  // Providing a nil function value will remove the existing handler (if set).
   271  // The handler may return false to return the selection to its original state.
   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  // SetConfirmedFunc sets a handler which is called when the list selection is confirmed.
   280  // Providing a nil function value will remove the existing handler (if set).
   281  func (l *List) SetConfirmedFunc(f func(index int)) {
   282  	l.Lock()
   283  	defer l.Unlock()
   284  
   285  	l.confirmedFunc = f
   286  }
   287  
   288  // Children returns the children of the widget. Children are drawn in the
   289  // order they are returned. Keyboard and mouse events are passed to children
   290  // in reverse order.
   291  func (l *List) Children() []Widget {
   292  	l.Lock()
   293  	defer l.Unlock()
   294  
   295  	return l.grid.Children()
   296  }
   297  
   298  // AddChildAt adds a widget to the list at the specified position.
   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  // Rows returns the number of rows in the list.
   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  // clampOffset clamps the list offset.
   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  // Cursor returns the cursor shape shown when a mouse cursor hovers over the
   361  // widget, or -1 to let widgets beneath determine the cursor shape.
   362  func (l *List) Cursor() ebiten.CursorShapeType {
   363  	return ebiten.CursorShapeDefault
   364  }
   365  
   366  // HandleKeyboard is called when a keyboard event occurs.
   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  		// Handle confirmation.
   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  		// Handle movement.
   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  // SetDrawBorder enables or disables borders being drawn around the list.
   423  func (l *List) SetDrawBorder(drawBorder bool) {
   424  	l.drawBorder = drawBorder
   425  }
   426  
   427  // HandleMouse is called when a mouse event occurs. Only mouse events that
   428  // are on top of the widget are passed to the widget.
   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  // Draw draws the widget on the screen.
   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  	// Draw grid.
   552  	err := l.grid.Draw(screen)
   553  	if err != nil {
   554  		return err
   555  	}
   556  
   557  	// Highlight selection.
   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  	// Draw border.
   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  	// Draw scroll bar.
   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  	// Draw scroll handle border.
   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  // Clear clears all items from the list.
   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