...

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  // Clip returns whether the widget and its children are restricted to drawing
   165  // within the widget's rect area of the screen. For best performance, Clip
   166  // should return false unless clipping is actually needed.
   167  func (l *List) Clip() bool {
   168  	return true
   169  }
   170  
   171  // SetColumnSizes sets the size of each column. A size of -1 represents an equal
   172  // proportion of the available space.
   173  func (l *List) SetColumnSizes(size ...int) {
   174  	l.Lock()
   175  	defer l.Unlock()
   176  
   177  	l.grid.SetColumnSizes(size...)
   178  }
   179  
   180  // SetItemHeight sets the height of the list items.
   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  // SetSelectionMode sets the selection mode of the list.
   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  // SetHighlightColor sets the color used to highlight the currently selected item.
   212  func (l *List) SetHighlightColor(c color.RGBA) {
   213  	l.Lock()
   214  	defer l.Unlock()
   215  
   216  	l.highlightColor = c
   217  }
   218  
   219  // SelectedItem returns the selected list item.
   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  // SetSelectedItem sets the selected list item.
   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  // SetScrollBarWidth sets the width of the scroll bar.
   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  // SetScrollBarColors sets the color of the scroll bar area and handle.
   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  // SetScrollBorderSize sets the size of the border around the scroll bar handle.
   257  func (l *List) SetScrollBorderSize(size int) {
   258  	l.Lock()
   259  	defer l.Unlock()
   260  
   261  	l.scrollBorderSize = size
   262  }
   263  
   264  // SetScrollBorderColor sets the color of the top, right, bottom and left border
   265  // of the scroll bar handle.
   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  // SetSelectedFunc sets a handler which is called when a list item is selected.
   277  // Providing a nil function value will remove the existing handler (if set).
   278  // The handler may return false to return the selection to its original state.
   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  // SetConfirmedFunc sets a handler which is called when the list selection is confirmed.
   287  // Providing a nil function value will remove the existing handler (if set).
   288  func (l *List) SetConfirmedFunc(f func(index int)) {
   289  	l.Lock()
   290  	defer l.Unlock()
   291  
   292  	l.confirmedFunc = f
   293  }
   294  
   295  // Children returns the children of the widget. Children are drawn in the
   296  // order they are returned. Keyboard and mouse events are passed to children
   297  // in reverse order.
   298  func (l *List) Children() []Widget {
   299  	l.Lock()
   300  	defer l.Unlock()
   301  
   302  	return l.grid.Children()
   303  }
   304  
   305  // AddChildAt adds a widget to the list at the specified position.
   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  // Rows returns the number of rows in the list.
   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  // clampOffset clamps the list offset.
   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  // Cursor returns the cursor shape shown when a mouse cursor hovers over the
   368  // widget, or -1 to let widgets beneath determine the cursor shape.
   369  func (l *List) Cursor() ebiten.CursorShapeType {
   370  	return ebiten.CursorShapeDefault
   371  }
   372  
   373  // HandleKeyboard is called when a keyboard event occurs.
   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  		// Handle confirmation.
   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  		// Handle movement.
   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  // SetDrawBorder enables or disables borders being drawn around the list.
   430  func (l *List) SetDrawBorder(drawBorder bool) {
   431  	l.drawBorder = drawBorder
   432  }
   433  
   434  // HandleMouse is called when a mouse event occurs. Only mouse events that
   435  // are on top of the widget are passed to the widget.
   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  // Draw draws the widget on the screen.
   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  	// Draw grid.
   559  	err := l.grid.Draw(screen)
   560  	if err != nil {
   561  		return err
   562  	}
   563  
   564  	// Highlight selection.
   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  	// Draw border.
   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  	// Draw scroll bar.
   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  	// Draw scroll handle border.
   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  // Clear clears all items from the list.
   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