...

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  
     9  	"github.com/hajimehoshi/ebiten/v2"
    10  )
    11  
    12  // SelectionMode represents a mode of selection.
    13  type SelectionMode int
    14  
    15  // Selection modes.
    16  const (
    17  	// SelectNone disables selection.
    18  	SelectNone SelectionMode = iota
    19  
    20  	// SelectRow enables selection by row.
    21  	SelectRow
    22  
    23  	// SelectColumn enables selection by column.
    24  	SelectColumn
    25  )
    26  
    27  // List is a list of widgets. Rows or cells may optionally be selectable.
    28  type List struct {
    29  	grid                 *Grid
    30  	itemHeight           int
    31  	highlightColor       color.RGBA
    32  	maxY                 int
    33  	selectionMode        SelectionMode
    34  	selectedX, selectedY int
    35  	selectedFunc         func(index int) (accept bool)
    36  	items                [][]Widget
    37  	offset               int
    38  	recreateGrid         bool
    39  	scrollRect           image.Rectangle
    40  	scrollWidth          int
    41  	scrollAreaColor      color.RGBA
    42  	scrollHandleColor    color.RGBA
    43  	scrollDrag           bool
    44  	drawBorder           bool
    45  	sync.Mutex
    46  }
    47  
    48  const (
    49  	initialPadding     = 5
    50  	initialScrollWidth = 32
    51  )
    52  
    53  var (
    54  	initialForeground   = color.RGBA{0, 0, 0, 255}
    55  	initialBackground   = color.RGBA{255, 255, 255, 255}
    56  	initialScrollArea   = color.RGBA{200, 200, 200, 255}
    57  	initialScrollHandle = color.RGBA{108, 108, 108, 255}
    58  )
    59  
    60  // NewList returns a new List widget.
    61  func NewList(itemHeight int, onSelected func(index int) (accept bool)) *List {
    62  	return &List{
    63  		grid:              NewGrid(),
    64  		itemHeight:        itemHeight,
    65  		highlightColor:    color.RGBA{255, 255, 255, 255},
    66  		maxY:              -1,
    67  		selectedY:         -1,
    68  		selectedFunc:      onSelected,
    69  		recreateGrid:      true,
    70  		scrollWidth:       initialScrollWidth,
    71  		scrollAreaColor:   initialScrollArea,
    72  		scrollHandleColor: initialScrollHandle,
    73  	}
    74  }
    75  
    76  // Rect returns the position and size of the widget.
    77  func (l *List) Rect() image.Rectangle {
    78  	l.Lock()
    79  	defer l.Unlock()
    80  
    81  	return l.grid.Rect()
    82  }
    83  
    84  // SetRect sets the position and size of the widget.
    85  func (l *List) SetRect(r image.Rectangle) {
    86  	l.Lock()
    87  	defer l.Unlock()
    88  
    89  	l.grid.SetRect(r)
    90  	l.recreateGrid = true
    91  }
    92  
    93  // Background returns the background color of the widget.
    94  func (l *List) Background() color.RGBA {
    95  	l.Lock()
    96  	defer l.Unlock()
    97  
    98  	return l.grid.Background()
    99  }
   100  
   101  // SetBackground sets the background color of the widget.
   102  func (l *List) SetBackground(background color.RGBA) {
   103  	l.Lock()
   104  	defer l.Unlock()
   105  
   106  	l.grid.SetBackground(background)
   107  }
   108  
   109  // Focus returns the focus state of the widget.
   110  func (l *List) Focus() bool {
   111  	l.Lock()
   112  	defer l.Unlock()
   113  
   114  	return l.grid.Focus()
   115  }
   116  
   117  // SetFocus sets the focus state of the widget.
   118  func (l *List) SetFocus(focus bool) (accept bool) {
   119  	l.Lock()
   120  	defer l.Unlock()
   121  
   122  	return l.grid.SetFocus(focus)
   123  }
   124  
   125  // Visible returns the visibility of the widget.
   126  func (l *List) Visible() bool {
   127  	l.Lock()
   128  	defer l.Unlock()
   129  
   130  	return l.grid.Visible()
   131  }
   132  
   133  // SetVisible sets the visibility of the widget.
   134  func (l *List) SetVisible(visible bool) {
   135  	l.Lock()
   136  	defer l.Unlock()
   137  
   138  	l.grid.SetVisible(visible)
   139  }
   140  
   141  // SetColumnSizes sets the size of each column. A size of -1 represents an equal
   142  // proportion of the available space.
   143  func (l *List) SetColumnSizes(size ...int) {
   144  	l.Lock()
   145  	defer l.Unlock()
   146  
   147  	l.grid.SetColumnSizes(size...)
   148  }
   149  
   150  // SetItemHeight sets the height of the list items.
   151  func (l *List) SetItemHeight(itemHeight int) {
   152  	l.Lock()
   153  	defer l.Unlock()
   154  
   155  	if l.itemHeight == itemHeight {
   156  		return
   157  	}
   158  	l.itemHeight = itemHeight
   159  
   160  	if l.maxY == -1 {
   161  		return
   162  	}
   163  	rowSizes := make([]int, l.maxY+1)
   164  	for i := range rowSizes {
   165  		rowSizes[i] = l.itemHeight
   166  	}
   167  	l.grid.SetRowSizes(rowSizes...)
   168  }
   169  
   170  // SetSelectionMode sets the selection mode of the list.
   171  func (l *List) SetSelectionMode(selectionMode SelectionMode) {
   172  	l.Lock()
   173  	defer l.Unlock()
   174  
   175  	if l.selectionMode == selectionMode {
   176  		return
   177  	}
   178  	l.selectionMode = selectionMode
   179  }
   180  
   181  // SetHighlightColor sets the color used to highlight the currently selected item.
   182  func (l *List) SetHighlightColor(c color.RGBA) {
   183  	l.Lock()
   184  	defer l.Unlock()
   185  
   186  	l.highlightColor = c
   187  }
   188  
   189  // SelectedItem returns the selected list item.
   190  func (l *List) SelectedItem() (x int, y int) {
   191  	l.Lock()
   192  	defer l.Unlock()
   193  
   194  	return l.selectedX, l.selectedY
   195  }
   196  
   197  // SetSelectedItem sets the selected list item.
   198  func (l *List) SetSelectedItem(x int, y int) {
   199  	l.Lock()
   200  	defer l.Unlock()
   201  
   202  	l.selectedX, l.selectedY = x, y
   203  }
   204  
   205  // SetScrollBarWidth sets the width of the scroll bar.
   206  func (l *List) SetScrollBarWidth(width int) {
   207  	l.Lock()
   208  	defer l.Unlock()
   209  
   210  	if l.scrollWidth == width {
   211  		return
   212  	}
   213  
   214  	l.scrollWidth = width
   215  }
   216  
   217  // SetScrollBarColors sets the color of the scroll bar area and handle.
   218  func (l *List) SetScrollBarColors(area color.RGBA, handle color.RGBA) {
   219  	l.Lock()
   220  	defer l.Unlock()
   221  
   222  	l.scrollAreaColor, l.scrollHandleColor = area, handle
   223  }
   224  
   225  // SetSelectedFunc sets a handler which is called when a list item is selected.
   226  // Providing a nil function value will remove the existing handler (if set).
   227  // The handler may return false to return the selection to its original state.
   228  func (l *List) SetSelectedFunc(f func(index int) (accept bool)) {
   229  	l.Lock()
   230  	defer l.Unlock()
   231  
   232  	l.selectedFunc = f
   233  }
   234  
   235  // Children returns the children of the widget. Children are drawn in the
   236  // order they are returned. Keyboard and mouse events are passed to children
   237  // in reverse order.
   238  func (l *List) Children() []Widget {
   239  	l.Lock()
   240  	defer l.Unlock()
   241  
   242  	return l.grid.Children()
   243  }
   244  
   245  // AddChildAt adds a widget to the list at the specified position.
   246  func (l *List) AddChildAt(w Widget, x int, y int) {
   247  	l.Lock()
   248  	defer l.Unlock()
   249  
   250  	for i := y; i >= len(l.items); i-- {
   251  		l.items = append(l.items, nil)
   252  	}
   253  	for i := x; i > len(l.items[y]); i-- {
   254  		l.items[y] = append(l.items[y], nil)
   255  	}
   256  	l.items[y] = append(l.items[y], &ignoreMouse{w})
   257  	if y > l.maxY {
   258  		l.maxY = y
   259  		l.recreateGrid = true
   260  	}
   261  }
   262  
   263  // Rows returns the number of rows in the list.
   264  func (l *List) Rows() int {
   265  	l.Lock()
   266  	defer l.Unlock()
   267  
   268  	return l.maxY + 1
   269  }
   270  
   271  func (l *List) showScrollBar() bool {
   272  	return len(l.items) > l.grid.rect.Dy()/l.itemHeight
   273  }
   274  
   275  // clampOffset clamps the list offset.
   276  func (l *List) clampOffset(offset int) int {
   277  	if offset >= len(l.items)-(l.grid.rect.Dy()/l.itemHeight) {
   278  		offset = len(l.items) - (l.grid.rect.Dy() / l.itemHeight)
   279  	}
   280  	if offset < 0 {
   281  		offset = 0
   282  	}
   283  	return offset
   284  }
   285  
   286  // HandleKeyboard is called when a keyboard event occurs.
   287  func (l *List) HandleKeyboard(key ebiten.Key, r rune) (handled bool, err error) {
   288  	l.Lock()
   289  	defer l.Unlock()
   290  
   291  	return l.grid.HandleKeyboard(key, r)
   292  }
   293  
   294  // SetDrawBorder enables or disables borders being drawn around the list.
   295  func (l *List) SetDrawBorder(drawBorder bool) {
   296  	l.drawBorder = drawBorder
   297  }
   298  
   299  // HandleMouse is called when a mouse event occurs. Only mouse events that
   300  // are on top of the widget are passed to the widget.
   301  func (l *List) HandleMouse(cursor image.Point, pressed bool, clicked bool) (handled bool, err error) {
   302  	l.Lock()
   303  	defer l.Unlock()
   304  
   305  	_, scroll := ebiten.Wheel()
   306  	if scroll != 0 {
   307  		offset := l.clampOffset(l.offset - int(math.Round(scroll)))
   308  		if offset != l.offset {
   309  			l.offset = offset
   310  			l.recreateGrid = true
   311  		}
   312  	}
   313  
   314  	if l.showScrollBar() && (pressed || l.scrollDrag) {
   315  		if pressed && cursor.In(l.scrollRect) {
   316  			dragY := cursor.Y - l.grid.rect.Min.Y
   317  			if dragY < 0 {
   318  				dragY = 0
   319  			} else if dragY > l.scrollRect.Dy() {
   320  				dragY = l.scrollRect.Dy()
   321  			}
   322  
   323  			pct := float64(dragY) / float64(l.scrollRect.Dy())
   324  			if pct < 0 {
   325  				pct = 0
   326  			} else if pct > 1 {
   327  				pct = 1
   328  			}
   329  
   330  			lastOffset := l.offset
   331  			offset := l.clampOffset(int(math.Round(float64(len(l.items)-(l.grid.rect.Dy()/l.itemHeight)) * pct)))
   332  			if offset != lastOffset {
   333  				l.offset = offset
   334  				l.recreateGrid = true
   335  			}
   336  			l.scrollDrag = true
   337  			return true, nil
   338  		} else if !pressed {
   339  			l.scrollDrag = false
   340  		}
   341  	}
   342  
   343  	if !clicked || (cursor.X == 0 && cursor.Y == 0) {
   344  		return true, nil
   345  	}
   346  	selected := (cursor.Y - l.grid.rect.Min.Y) / l.itemHeight
   347  	if selected >= 0 && selected <= l.maxY {
   348  		lastSelected := l.selectedY
   349  		l.selectedY = selected
   350  		if l.selectedFunc != nil {
   351  			accept := l.selectedFunc(l.selectedY)
   352  			if !accept {
   353  				l.selectedY = lastSelected
   354  			}
   355  		}
   356  	}
   357  	return true, nil
   358  }
   359  
   360  // Draw draws the widget on the screen.
   361  func (l *List) Draw(screen *ebiten.Image) error {
   362  	l.Lock()
   363  	defer l.Unlock()
   364  
   365  	if l.recreateGrid {
   366  		maxY := l.grid.rect.Dy() / l.itemHeight
   367  		l.offset = l.clampOffset(l.offset)
   368  		l.grid.Clear()
   369  		rowSizes := make([]int, l.maxY+1)
   370  		for i := range rowSizes {
   371  			rowSizes[i] = l.itemHeight
   372  		}
   373  		l.grid.SetRowSizes(rowSizes...)
   374  		var y int
   375  		for i := range l.items {
   376  			if i < l.offset {
   377  				continue
   378  			} else if y >= maxY {
   379  				break
   380  			}
   381  			for x := range l.items[i] {
   382  				w := l.items[i][x]
   383  				if w == nil {
   384  					continue
   385  				}
   386  				l.grid.AddChildAt(w, x, y, 1, 1)
   387  			}
   388  			y++
   389  		}
   390  		l.recreateGrid = false
   391  	}
   392  
   393  	// Draw grid.
   394  	err := l.grid.Draw(screen)
   395  	if err != nil {
   396  		return err
   397  	}
   398  
   399  	// Highlight selection.
   400  	drawHighlight := l.selectionMode != SelectNone && l.selectedY >= 0
   401  	if drawHighlight {
   402  		{
   403  			x, y := l.grid.rect.Min.X, l.grid.rect.Min.Y+l.selectedY*l.itemHeight
   404  			w, h := l.grid.rect.Dx(), l.itemHeight
   405  			r := image.Rect(x, y, x+w, y+h)
   406  			screen.SubImage(r).(*ebiten.Image).Fill(l.highlightColor)
   407  		}
   408  	}
   409  
   410  	// Draw border.
   411  	if l.drawBorder {
   412  		const borderSize = 4
   413  		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.BorderColorBottom)
   414  		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.BorderColorBottom)
   415  		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.BorderColorBottom)
   416  		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.BorderColorBottom)
   417  	}
   418  
   419  	// Draw scroll bar.
   420  	if !l.showScrollBar() {
   421  		return nil
   422  	}
   423  	w, h := l.grid.rect.Dx(), l.grid.rect.Dy()
   424  	scrollAreaX, scrollAreaY := l.grid.rect.Min.X+w-l.scrollWidth, l.grid.rect.Min.Y
   425  	l.scrollRect = image.Rect(scrollAreaX, scrollAreaY, scrollAreaX+l.scrollWidth, scrollAreaY+h)
   426  
   427  	scrollBarH := l.scrollWidth / 2
   428  	if scrollBarH < 4 {
   429  		scrollBarH = 4
   430  	}
   431  
   432  	scrollX, scrollY := l.grid.rect.Min.X+w-l.scrollWidth, l.grid.rect.Min.Y
   433  	pct := float64(-l.offset) / float64(len(l.items)-(l.grid.rect.Dy()/l.itemHeight))
   434  	scrollY -= int(float64(h-scrollBarH) * pct)
   435  	scrollBarRect := image.Rect(scrollX, scrollY, scrollX+l.scrollWidth, scrollY+scrollBarH)
   436  
   437  	screen.SubImage(l.scrollRect).(*ebiten.Image).Fill(l.scrollAreaColor)
   438  	screen.SubImage(scrollBarRect).(*ebiten.Image).Fill(l.scrollHandleColor)
   439  	return nil
   440  }
   441  
   442  // Clear clears all items from the list.
   443  func (l *List) Clear() {
   444  	l.Lock()
   445  	defer l.Unlock()
   446  
   447  	l.items = nil
   448  	l.maxY = -1
   449  	l.selectedX, l.selectedY = 0, -1
   450  	l.offset = 0
   451  	l.recreateGrid = true
   452  }
   453  

View as plain text