...

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. Rows or cells may optionally be selectable.
    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{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  // Rect returns the position and size of the widget.
    92  func (l *List) Rect() image.Rectangle {
    93  	l.Lock()
    94  	defer l.Unlock()
    95  
    96  	return l.rect
    97  }
    98  
    99  // SetRect sets the position and size of the widget.
   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  // Background returns the background color of the widget.
   114  func (l *List) Background() color.RGBA {
   115  	l.Lock()
   116  	defer l.Unlock()
   117  
   118  	return l.grid.Background()
   119  }
   120  
   121  // SetBackground sets the background color of the widget.
   122  func (l *List) SetBackground(background color.RGBA) {
   123  	l.Lock()
   124  	defer l.Unlock()
   125  
   126  	l.grid.SetBackground(background)
   127  }
   128  
   129  // Focus returns the focus state of the widget.
   130  func (l *List) Focus() bool {
   131  	l.Lock()
   132  	defer l.Unlock()
   133  
   134  	return l.focused
   135  }
   136  
   137  // SetFocus sets the focus state of the widget.
   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  // Visible returns the visibility of the widget.
   147  func (l *List) Visible() bool {
   148  	l.Lock()
   149  	defer l.Unlock()
   150  
   151  	return l.grid.Visible()
   152  }
   153  
   154  // SetVisible sets the visibility of the widget.
   155  func (l *List) SetVisible(visible bool) {
   156  	l.Lock()
   157  	defer l.Unlock()
   158  
   159  	l.grid.SetVisible(visible)
   160  }
   161  
   162  // SetColumnSizes sets the size of each column. A size of -1 represents an equal
   163  // proportion of the available space.
   164  func (l *List) SetColumnSizes(size ...int) {
   165  	l.Lock()
   166  	defer l.Unlock()
   167  
   168  	l.grid.SetColumnSizes(size...)
   169  }
   170  
   171  // SetItemHeight sets the height of the list items.
   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  // SetSelectionMode sets the selection mode of the list.
   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  // SetHighlightColor sets the color used to highlight the currently selected item.
   203  func (l *List) SetHighlightColor(c color.RGBA) {
   204  	l.Lock()
   205  	defer l.Unlock()
   206  
   207  	l.highlightColor = c
   208  }
   209  
   210  // SelectedItem returns the selected list item.
   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  // SetSelectedItem sets the selected list item.
   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  // SetScrollBarWidth sets the width of the scroll bar.
   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  // SetScrollBarColors sets the color of the scroll bar area and handle.
   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  // SetScrollBorderSize sets the size of the border around the scroll bar handle.
   248  func (l *List) SetScrollBorderSize(size int) {
   249  	l.Lock()
   250  	defer l.Unlock()
   251  
   252  	l.scrollBorderSize = size
   253  }
   254  
   255  // SetScrollBorderColor sets the color of the top, right, bottom and left border
   256  // of the scroll bar handle.
   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  // SetSelectedFunc sets a handler which is called when a list item is selected.
   268  // Providing a nil function value will remove the existing handler (if set).
   269  // The handler may return false to return the selection to its original state.
   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  // SetConfirmedFunc sets a handler which is called when the list selection is confirmed.
   278  // Providing a nil function value will remove the existing handler (if set).
   279  func (l *List) SetConfirmedFunc(f func(index int)) {
   280  	l.Lock()
   281  	defer l.Unlock()
   282  
   283  	l.confirmedFunc = f
   284  }
   285  
   286  // Children returns the children of the widget. Children are drawn in the
   287  // order they are returned. Keyboard and mouse events are passed to children
   288  // in reverse order.
   289  func (l *List) Children() []Widget {
   290  	l.Lock()
   291  	defer l.Unlock()
   292  
   293  	return l.grid.Children()
   294  }
   295  
   296  // AddChildAt adds a widget to the list at the specified position.
   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  // Rows returns the number of rows in the list.
   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  // clampOffset clamps the list offset.
   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  // Cursor returns the cursor shape shown when a mouse cursor hovers over the
   359  // widget, or -1 to let widgets beneath determine the cursor shape.
   360  func (l *List) Cursor() ebiten.CursorShapeType {
   361  	return ebiten.CursorShapeDefault
   362  }
   363  
   364  // HandleKeyboard is called when a keyboard event occurs.
   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  		// Handle confirmation.
   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  		// Handle movement.
   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  // SetDrawBorder enables or disables borders being drawn around the list.
   421  func (l *List) SetDrawBorder(drawBorder bool) {
   422  	l.drawBorder = drawBorder
   423  }
   424  
   425  // HandleMouse is called when a mouse event occurs. Only mouse events that
   426  // are on top of the widget are passed to the widget.
   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  // Draw draws the widget on the screen.
   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  	// Draw grid.
   545  	err := l.grid.Draw(screen.SubImage(l.rect).(*ebiten.Image))
   546  	if err != nil {
   547  		return err
   548  	}
   549  
   550  	// Highlight selection.
   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  	// Draw border.
   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  	// Draw scroll bar.
   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  	// Draw scroll handle border.
   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  // Clear clears all items from the list.
   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