...

Source file src/codeberg.org/tslocum/etk/list.go

Documentation: codeberg.org/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  	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  // NewList returns a new List widget.
    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  // Rect returns the position and size of the widget.
    95  func (l *List) Rect() image.Rectangle {
    96  	l.Lock()
    97  	defer l.Unlock()
    98  
    99  	return l.rect
   100  }
   101  
   102  // SetRect sets the position and size of the widget.
   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  // Background returns the background color of the widget.
   115  func (l *List) Background() color.RGBA {
   116  	l.Lock()
   117  	defer l.Unlock()
   118  
   119  	return l.grid.Background()
   120  }
   121  
   122  // SetBackground sets the background color of the widget.
   123  func (l *List) SetBackground(background color.RGBA) {
   124  	l.Lock()
   125  	defer l.Unlock()
   126  
   127  	l.grid.SetBackground(background)
   128  }
   129  
   130  // Focus returns the focus state of the widget.
   131  func (l *List) Focus() bool {
   132  	l.Lock()
   133  	defer l.Unlock()
   134  
   135  	return l.focused
   136  }
   137  
   138  // SetFocus sets the focus state of the widget.
   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  // Visible returns the visibility of the widget.
   148  func (l *List) Visible() bool {
   149  	l.Lock()
   150  	defer l.Unlock()
   151  
   152  	return l.grid.Visible()
   153  }
   154  
   155  // SetVisible sets the visibility of the widget.
   156  func (l *List) SetVisible(visible bool) {
   157  	l.Lock()
   158  	defer l.Unlock()
   159  
   160  	l.grid.SetVisible(visible)
   161  }
   162  
   163  // Clip returns whether the widget and its children are restricted to drawing
   164  // within the widget's rect area of the screen. For best performance, Clip
   165  // should return false unless clipping is actually needed.
   166  func (l *List) Clip() bool {
   167  	return true
   168  }
   169  
   170  // SetColumnSizes sets the size of each column. A size of -1 represents an equal
   171  // proportion of the available space.
   172  func (l *List) SetColumnSizes(size ...int) {
   173  	l.Lock()
   174  	defer l.Unlock()
   175  
   176  	l.grid.SetColumnSizes(size...)
   177  }
   178  
   179  // SetItemHeight sets the height of the list items.
   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  // SetSelectionMode sets the selection mode of the list.
   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  // SetHighlightColor sets the color used to highlight the currently selected item.
   211  func (l *List) SetHighlightColor(c color.RGBA) {
   212  	l.Lock()
   213  	defer l.Unlock()
   214  
   215  	l.highlightColor = c
   216  }
   217  
   218  // SelectedItem returns the selected list item.
   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  // SetSelectedItem sets the selected list item.
   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  // SetScrollBarWidth sets the width of the scroll bar.
   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  // SetScrollBarColors sets the color of the scroll bar area and handle.
   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  // SetScrollBorderSize sets the size of the border around the scroll bar handle.
   255  func (l *List) SetScrollBorderSize(size int) {
   256  	l.Lock()
   257  	defer l.Unlock()
   258  
   259  	l.scrollBorderSize = size
   260  }
   261  
   262  // SetScrollBorderColor sets the color of the top, right, bottom and left border
   263  // of the scroll bar handle.
   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  // SetChangeFunc sets a handler which is called when the selected item changes.
   275  // Providing a nil function value will remove the existing handler (if set).
   276  // The handler may return false to return the selection to its original state.
   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  // SetConfirmFunc sets a handler which is called when the list selection is confirmed.
   285  // Providing a nil function value will remove the existing handler (if set).
   286  func (l *List) SetConfirmFunc(onConfirm func(index int)) {
   287  	l.Lock()
   288  	defer l.Unlock()
   289  
   290  	l.onConfirm = onConfirm
   291  }
   292  
   293  // Children returns the children of the widget. Children are drawn in the
   294  // order they are returned. Keyboard and mouse events are passed to children
   295  // in reverse order.
   296  func (l *List) Children() []Widget {
   297  	l.Lock()
   298  	defer l.Unlock()
   299  
   300  	return l.grid.Children()
   301  }
   302  
   303  // AddChildAt adds a widget to the list at the specified position.
   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  // Rows returns the number of rows in the list.
   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  // clampOffset clamps the list offset.
   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  // Cursor returns the cursor shape shown when a mouse cursor hovers over the
   350  // widget, or -1 to let widgets beneath determine the cursor shape.
   351  func (l *List) Cursor() ebiten.CursorShapeType {
   352  	return ebiten.CursorShapeDefault
   353  }
   354  
   355  // HandleKeyboard is called when a keyboard event occurs.
   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  		// Handle confirmation.
   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  		// Handle movement.
   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  // SetDrawBorder enables or disables borders being drawn around the list.
   411  func (l *List) SetDrawBorder(drawBorder bool) {
   412  	l.drawBorder = drawBorder
   413  }
   414  
   415  // HandleMouse is called when a mouse event occurs. Only mouse events that
   416  // are on top of the widget are passed to the widget.
   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  // Draw draws the widget on the screen.
   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  	// Draw grid.
   548  	err := l.grid.Draw(screen)
   549  	if err != nil {
   550  		return err
   551  	}
   552  
   553  	// Highlight selection.
   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  	// Draw border.
   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  	// Draw scroll bar.
   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  	// Draw scroll handle border.
   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  // Clear clears all items from the list.
   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