...

Source file src/code.rocketnine.space/tslocum/messeji/textfield.go

Documentation: code.rocketnine.space/tslocum/messeji

     1  package messeji
     2  
     3  import (
     4  	"image"
     5  	"image/color"
     6  	"strings"
     7  	"sync"
     8  	"unicode"
     9  
    10  	"github.com/hajimehoshi/ebiten/v2"
    11  	"github.com/hajimehoshi/ebiten/v2/inpututil"
    12  	"github.com/hajimehoshi/ebiten/v2/text"
    13  	"golang.org/x/image/font"
    14  )
    15  
    16  // Alignment specifies how text is aligned within the field.
    17  type Alignment int
    18  
    19  const (
    20  	// AlignStart aligns text at the start of the field.
    21  	AlignStart Alignment = 0
    22  
    23  	// AlignCenter aligns text at the center of the field.
    24  	AlignCenter Alignment = 1
    25  
    26  	// AlignEnd aligns text at the end of the field.
    27  	AlignEnd Alignment = 2
    28  )
    29  
    30  const (
    31  	initialPadding     = 5
    32  	initialScrollWidth = 32
    33  )
    34  
    35  var (
    36  	initialForeground = color.RGBA{0, 0, 0, 255}
    37  	initialBackground = color.RGBA{255, 255, 255, 255}
    38  )
    39  
    40  // TextField is a text display field. Call Update and Draw when your Game's
    41  // Update and Draw methods are called.
    42  //
    43  // Note: A position and size must be set via SetRect before the field will appear.
    44  // Keyboard events are not handled by default, and may be enabled via SetHandleKeyboard.
    45  type TextField struct {
    46  	// r specifies the position and size of the field.
    47  	r image.Rectangle
    48  
    49  	// buffer is the actual content of the field.
    50  	buffer string
    51  
    52  	// prefix is the text shown before the content of the field.
    53  	prefix string
    54  
    55  	// suffix is the text shown after the content of the field.
    56  	suffix string
    57  
    58  	// wordWrap determines whether content is wrapped at word boundaries.
    59  	wordWrap bool
    60  
    61  	// bufferWrapped is the content of the field as it appears on the screen.
    62  	bufferWrapped []string
    63  
    64  	// bufferSize is the size (in pixels) of the entire text buffer. When single
    65  	// line mode is enabled,
    66  	bufferSize int
    67  
    68  	// lineWidths is the size (in pixels) of each line as it appears on the screen.
    69  	lineWidths []int
    70  
    71  	// singleLine is whether the field displays all text on a single line.
    72  	singleLine bool
    73  
    74  	// horizontal is the horizontal alignment of the text within field.
    75  	horizontal Alignment
    76  
    77  	// vertical is the vertical alignment of the text within field.
    78  	vertical Alignment
    79  
    80  	// face is the font face of the text within the field.
    81  	face font.Face
    82  
    83  	// lineHeight is the height of a single line of text.
    84  	lineHeight int
    85  
    86  	// overrideLineHeight is the custom height for a line of text, or 0 to disable.
    87  	overrideLineHeight int
    88  
    89  	// lineOffset is the offset of the baseline current font.
    90  	lineOffset int
    91  
    92  	// textColor is the color of the text within the field.
    93  	textColor color.Color
    94  
    95  	// backgroundColor is the color of the background of the field.
    96  	backgroundColor color.Color
    97  
    98  	// padding is the amount of padding around the text within the field.
    99  	padding int
   100  
   101  	// follow determines whether the field should automatically scroll to the
   102  	// end when content is added to the buffer.
   103  	follow bool
   104  
   105  	// overflow is whether the content of the field is currently larger than the field.
   106  	overflow bool
   107  
   108  	// offset is the current view offset of the text within the field.
   109  	offset int
   110  
   111  	// handleKeyboard is a flag which, when enabled, causes keyboard input to be handled.
   112  	handleKeyboard bool
   113  
   114  	// modified is a flag which, when enabled, causes bufferModified to be called
   115  	// during the next Draw call.
   116  	modified bool
   117  
   118  	// scrollRect specifies the position and size of the scrolling area.
   119  	scrollRect image.Rectangle
   120  
   121  	// scrollWidth is the width of the scroll bar.
   122  	scrollWidth int
   123  
   124  	// scrollVisible is whether the scroll bar is visible on the screen.
   125  	scrollVisible bool
   126  
   127  	// scrollAutoHide is whether the scroll bar should be automatically hidden
   128  	// when the entire text buffer fits within the screen.
   129  	scrollAutoHide bool
   130  
   131  	// scrollDrag is whether the scroll bar is currently being dragged.
   132  	scrollDrag bool
   133  
   134  	// img is the image of the field.
   135  	img *ebiten.Image
   136  
   137  	// visible is whether the field is visible on the screen.
   138  	visible bool
   139  
   140  	sync.Mutex
   141  }
   142  
   143  // NewTextField returns a new TextField. See type documentation for more info.
   144  func NewTextField(face font.Face) *TextField {
   145  	f := &TextField{
   146  		face:            face,
   147  		textColor:       initialForeground,
   148  		backgroundColor: initialBackground,
   149  		padding:         initialPadding,
   150  		scrollWidth:     initialScrollWidth,
   151  		follow:          true,
   152  		wordWrap:        true,
   153  		scrollVisible:   true,
   154  		scrollAutoHide:  true,
   155  		visible:         true,
   156  	}
   157  	f.fontUpdated()
   158  	return f
   159  }
   160  
   161  // Rect returns the position and size of the field.
   162  func (f *TextField) Rect() image.Rectangle {
   163  	f.Lock()
   164  	defer f.Unlock()
   165  
   166  	return f.r
   167  }
   168  
   169  // SetRect sets the position and size of the field.
   170  func (f *TextField) SetRect(r image.Rectangle) {
   171  	f.Lock()
   172  	defer f.Unlock()
   173  
   174  	f.r = r
   175  	f.drawImage()
   176  }
   177  
   178  // Text returns the text in the field.
   179  func (f *TextField) Text() string {
   180  	f.Lock()
   181  	defer f.Unlock()
   182  
   183  	return f.buffer
   184  }
   185  
   186  // SetText sets the text in the field.
   187  func (f *TextField) SetText(text string) {
   188  	f.Lock()
   189  	defer f.Unlock()
   190  
   191  	f.buffer = text
   192  	f.modified = true
   193  }
   194  
   195  // SetPrefix sets the text shown before the content of the field.
   196  func (f *TextField) SetPrefix(text string) {
   197  	f.Lock()
   198  	defer f.Unlock()
   199  
   200  	f.prefix = text
   201  	f.drawImage()
   202  }
   203  
   204  // SetSuffix sets the text shown before the content of the field.
   205  func (f *TextField) SetSuffix(text string) {
   206  	f.Lock()
   207  	defer f.Unlock()
   208  
   209  	f.suffix = text
   210  	f.drawImage()
   211  }
   212  
   213  // SetFollow sets whether the field should automatically scroll to the end when
   214  // content is added to the buffer.
   215  func (f *TextField) SetFollow(follow bool) {
   216  	f.Lock()
   217  	defer f.Unlock()
   218  
   219  	f.follow = follow
   220  }
   221  
   222  // SetSingleLine sets whether the field displays all text on a single line.
   223  // When enabled, the field scrolls horizontally. Otherwise, it scrolls vertically.
   224  func (f *TextField) SetSingleLine(single bool) {
   225  	f.Lock()
   226  	defer f.Unlock()
   227  
   228  	if f.singleLine == single {
   229  		return
   230  	}
   231  
   232  	f.singleLine = single
   233  	f.bufferModified()
   234  }
   235  
   236  // SetHorizontal sets the horizontal alignment of the text within the field.
   237  func (f *TextField) SetHorizontal(h Alignment) {
   238  	f.Lock()
   239  	defer f.Unlock()
   240  
   241  	if f.horizontal == h {
   242  		return
   243  	}
   244  
   245  	f.horizontal = h
   246  	f.bufferModified()
   247  }
   248  
   249  // SetVertical sets the veritcal alignment of the text within the field.
   250  func (f *TextField) SetVertical(v Alignment) {
   251  	f.Lock()
   252  	defer f.Unlock()
   253  
   254  	if f.vertical == v {
   255  		return
   256  	}
   257  
   258  	f.vertical = v
   259  	f.bufferModified()
   260  }
   261  
   262  // LineHeight returns the line height for the field.
   263  func (f *TextField) LineHeight() int {
   264  	f.Lock()
   265  	defer f.Unlock()
   266  
   267  	if f.overrideLineHeight != 0 {
   268  		return f.overrideLineHeight
   269  	}
   270  	return f.lineHeight
   271  }
   272  
   273  // SetLineHeight sets a custom line height for the field. Setting a line
   274  // height of 0 restores the automatic line height detection based on the font.
   275  func (f *TextField) SetLineHeight(height int) {
   276  	f.Lock()
   277  	defer f.Unlock()
   278  
   279  	f.overrideLineHeight = height
   280  }
   281  
   282  // SetForegroundColor sets the color of the text within the field.
   283  func (f *TextField) SetForegroundColor(c color.Color) {
   284  	f.Lock()
   285  	defer f.Unlock()
   286  
   287  	f.textColor = c
   288  }
   289  
   290  // SetBackgroundColor sets the color of the background of the field.
   291  func (f *TextField) SetBackgroundColor(c color.Color) {
   292  	f.Lock()
   293  	defer f.Unlock()
   294  
   295  	f.backgroundColor = c
   296  }
   297  
   298  // SetFont sets the font face of the text within the field.
   299  func (f *TextField) SetFont(face font.Face) {
   300  	f.Lock()
   301  	defer f.Unlock()
   302  
   303  	f.face = face
   304  	f.fontUpdated()
   305  }
   306  
   307  // Padding returns the amount of padding around the text within the field.
   308  func (f *TextField) Padding() int {
   309  	f.Lock()
   310  	defer f.Unlock()
   311  
   312  	return f.padding
   313  }
   314  
   315  // SetPadding sets the amount of padding around the text within the field.
   316  func (f *TextField) SetPadding(padding int) {
   317  	f.Lock()
   318  	defer f.Unlock()
   319  
   320  	f.padding = padding
   321  }
   322  
   323  // Visible returns whether the field is currently visible on the screen.
   324  func (f *TextField) Visible() bool {
   325  	return f.visible
   326  }
   327  
   328  // SetVisible sets whether the field is visible on the screen.
   329  func (f *TextField) SetVisible(visible bool) {
   330  	f.Lock()
   331  	defer f.Unlock()
   332  
   333  	if f.visible == visible {
   334  		return
   335  	}
   336  
   337  	f.visible = visible
   338  	if visible {
   339  		f.drawImage()
   340  	}
   341  }
   342  
   343  // SetScrollBarWidth sets the width of the scroll bar.
   344  func (f *TextField) SetScrollBarWidth(width int) {
   345  	f.Lock()
   346  	defer f.Unlock()
   347  
   348  	if f.scrollWidth == width {
   349  		return
   350  	}
   351  
   352  	f.scrollWidth = width
   353  	f.drawImage()
   354  }
   355  
   356  // SetScrollBarVisible sets whether the scroll bar is visible on the screen.
   357  func (f *TextField) SetScrollBarVisible(scrollVisible bool) {
   358  	f.Lock()
   359  	defer f.Unlock()
   360  
   361  	if f.scrollVisible == scrollVisible {
   362  		return
   363  	}
   364  
   365  	f.scrollVisible = scrollVisible
   366  	f.drawImage()
   367  }
   368  
   369  // SetAutoHideScrollBar sets whether the scroll bar is automatically hidden
   370  // when the entire text buffer is visible.
   371  func (f *TextField) SetAutoHideScrollBar(autoHide bool) {
   372  	f.Lock()
   373  	defer f.Unlock()
   374  
   375  	if f.scrollAutoHide == autoHide {
   376  		return
   377  	}
   378  
   379  	f.scrollAutoHide = autoHide
   380  	f.drawImage()
   381  }
   382  
   383  // WordWrap returns the current text wrap mode.
   384  func (f *TextField) WordWrap() bool {
   385  	f.Lock()
   386  	defer f.Unlock()
   387  
   388  	return f.wordWrap
   389  }
   390  
   391  // SetWordWrap sets a flag which, when enabled, causes text to wrap without breaking words.
   392  func (f *TextField) SetWordWrap(wrap bool) {
   393  	f.Lock()
   394  	defer f.Unlock()
   395  
   396  	if f.wordWrap == wrap {
   397  		return
   398  	}
   399  
   400  	f.wordWrap = wrap
   401  	f.drawImage()
   402  }
   403  
   404  // SetHandleKeyboard sets a flag controlling whether keyboard input should be handled
   405  // by the field. This can be used to facilitate focus changes between multiple inputs.
   406  func (f *TextField) SetHandleKeyboard(handle bool) {
   407  	f.Lock()
   408  	defer f.Unlock()
   409  
   410  	f.handleKeyboard = handle
   411  }
   412  
   413  // Write writes to the field's buffer.
   414  func (f *TextField) Write(p []byte) (n int, err error) {
   415  	f.Lock()
   416  	defer f.Unlock()
   417  
   418  	f.buffer += string(p)
   419  	f.modified = true
   420  	return len(p), nil
   421  }
   422  
   423  // Update updates the field. This function should be called when
   424  // Game.Update is called.
   425  func (f *TextField) Update() error {
   426  	f.Lock()
   427  	defer f.Unlock()
   428  
   429  	if !f.visible || rectIsZero(f.r) {
   430  		return nil
   431  	}
   432  
   433  	var redraw bool
   434  
   435  	// Handle keyboard PageUp/PageDown.
   436  	if f.handleKeyboard {
   437  		offsetAmount := 0
   438  		if inpututil.IsKeyJustPressed(ebiten.KeyPageUp) {
   439  			offsetAmount = -100
   440  		} else if inpututil.IsKeyJustPressed(ebiten.KeyPageDown) {
   441  			offsetAmount = 100
   442  		}
   443  		if offsetAmount != 0 {
   444  			f.offset += offsetAmount
   445  			f.clampOffset()
   446  			redraw = true
   447  		}
   448  	}
   449  
   450  	// Handle mouse wheel.
   451  	_, scrollY := ebiten.Wheel()
   452  	if scrollY != 0 {
   453  		x, y := ebiten.CursorPosition()
   454  		p := image.Point{x, y}
   455  		if p.In(f.r) {
   456  			const offsetAmount = 25
   457  			f.offset -= int(scrollY * offsetAmount)
   458  			f.clampOffset()
   459  			redraw = true
   460  		}
   461  	}
   462  
   463  	// Handle scroll bar click (and drag).
   464  	if f.showScrollBar() {
   465  		if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) || f.scrollDrag {
   466  			x, y := ebiten.CursorPosition()
   467  			p := image.Point{x - f.r.Min.X, y - f.r.Min.Y}
   468  			if f.scrollDrag || p.In(f.scrollRect) {
   469  				dragY := y - f.r.Min.Y - f.scrollWidth/4
   470  				if dragY < 0 {
   471  					dragY = 0
   472  				} else if dragY > f.scrollRect.Dy() {
   473  					dragY = f.scrollRect.Dy()
   474  				}
   475  				pct := float64(dragY) / float64(f.scrollRect.Dy()-f.scrollWidth/2)
   476  				if pct > 1 {
   477  					pct = 1
   478  				}
   479  				h := f.r.Dy()
   480  				f.offset = int(float64(f.bufferSize-h) * pct)
   481  				redraw = true
   482  				f.scrollDrag = true
   483  			}
   484  			if !ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
   485  				f.scrollDrag = false
   486  			}
   487  		}
   488  	}
   489  
   490  	if redraw {
   491  		f.drawImage()
   492  	}
   493  	return nil
   494  }
   495  
   496  // Draw draws the field on the screen. This function should be called
   497  // when Game.Draw is called.
   498  func (f *TextField) Draw(screen *ebiten.Image) {
   499  	f.Lock()
   500  	defer f.Unlock()
   501  
   502  	if f.modified {
   503  		f.bufferModified()
   504  		f.modified = false
   505  	}
   506  
   507  	if !f.visible || rectIsZero(f.r) || f.img == nil {
   508  		return
   509  	}
   510  
   511  	op := &ebiten.DrawImageOptions{}
   512  	op.GeoM.Translate(float64(f.r.Min.X), float64(f.r.Min.Y))
   513  	screen.DrawImage(f.img, op)
   514  }
   515  
   516  func (f *TextField) fontUpdated() {
   517  	m := f.face.Metrics()
   518  	f.lineHeight = m.Height.Round()
   519  	f.lineOffset = m.Ascent.Round()
   520  }
   521  
   522  func (f *TextField) wrapContent(withScrollBar bool) {
   523  	f.lineWidths = f.lineWidths[:0]
   524  	buffer := f.prefix + f.buffer + f.suffix
   525  
   526  	if f.singleLine {
   527  		buffer = strings.ReplaceAll(buffer, "\n", "")
   528  		bounds := text.BoundString(f.face, buffer)
   529  
   530  		f.bufferWrapped = []string{buffer}
   531  		f.lineWidths = append(f.lineWidths, bounds.Dx())
   532  		return
   533  	}
   534  
   535  	w := f.r.Dx()
   536  	if withScrollBar {
   537  		w -= f.scrollWidth
   538  	}
   539  	f.bufferWrapped = f.bufferWrapped[:0]
   540  	for _, line := range strings.Split(buffer, "\n") {
   541  		// BoundString returns 0 for strings containing only whitespace.
   542  		if strings.TrimSpace(line) == "" {
   543  			f.bufferWrapped = append(f.bufferWrapped, "")
   544  			f.lineWidths = append(f.lineWidths, 0)
   545  			continue
   546  		}
   547  
   548  		l := len(line)
   549  		availableWidth := w - (f.padding * 2)
   550  		var start int
   551  		var end int
   552  		var initialEnd int
   553  		for start < l {
   554  			for end = l; end > start; end-- {
   555  				initialEnd = end
   556  
   557  				bounds := text.BoundString(f.face, line[start:end])
   558  				if bounds.Dx() > availableWidth {
   559  					continue
   560  				}
   561  
   562  				if f.wordWrap && end < l && !unicode.IsSpace(rune(line[end])) {
   563  					for endOffset := 0; endOffset < end-start; endOffset++ {
   564  						if unicode.IsSpace(rune(line[end-endOffset])) {
   565  							end = end - endOffset
   566  							if end < l-1 {
   567  								end++
   568  							}
   569  							break
   570  						}
   571  					}
   572  				}
   573  
   574  				if end != initialEnd && f.horizontal != AlignStart {
   575  					bounds = text.BoundString(f.face, line[start:end])
   576  				}
   577  
   578  				f.bufferWrapped = append(f.bufferWrapped, line[start:end])
   579  				f.lineWidths = append(f.lineWidths, bounds.Dx())
   580  				break
   581  			}
   582  			start = end
   583  		}
   584  	}
   585  }
   586  
   587  // drawContent draws the text buffer to img.
   588  func (f *TextField) drawContent() (overflow bool) {
   589  	f.img.Fill(f.backgroundColor)
   590  
   591  	fieldWidth := f.r.Dx()
   592  	fieldHeight := f.r.Dy()
   593  	if f.showScrollBar() {
   594  		fieldWidth -= f.scrollWidth
   595  	}
   596  	lines := len(f.bufferWrapped)
   597  
   598  	h := f.r.Dy()
   599  	lineHeight := f.overrideLineHeight
   600  	if lineHeight == 0 {
   601  		lineHeight = f.lineHeight
   602  	}
   603  
   604  	f.bufferSize = 0
   605  	for i, line := range f.bufferWrapped {
   606  		lineX := f.padding
   607  		lineY := f.lineOffset + lineHeight*i
   608  
   609  		// Calculate buffer size (width or height).
   610  		if f.singleLine {
   611  			bounds := text.BoundString(f.face, line)
   612  			f.bufferSize = bounds.Dx() + f.padding*2
   613  		} else {
   614  			f.bufferSize = lineY + f.padding*2
   615  		}
   616  
   617  		// Calculate whether the line overflows the visible area.
   618  		lineOverflows := lineY < 0 || lineY >= h-(f.padding*2)
   619  		if lineOverflows {
   620  			overflow = true
   621  		}
   622  
   623  		// Skip drawing off-screen lines.
   624  		if lineY < 0 || lineY-lineHeight > f.offset+h {
   625  			continue
   626  		}
   627  
   628  		// Apply scrolling transformation.
   629  		if f.singleLine {
   630  			lineX -= f.offset
   631  		} else {
   632  			lineY -= f.offset
   633  		}
   634  
   635  		// Align horizontally.
   636  		if f.horizontal == AlignCenter {
   637  			lineX = (fieldWidth - f.lineWidths[i]) / 2
   638  		} else if f.horizontal == AlignEnd {
   639  			lineX = (fieldWidth - f.lineWidths[i]) - f.padding*2
   640  		}
   641  
   642  		// Align vertically.
   643  		if f.vertical == AlignCenter && lineHeight*lines <= h {
   644  			lineY = (fieldHeight-(lineHeight*lines))/2 + lineHeight*(i+1) - f.lineOffset
   645  		} else if f.vertical == AlignEnd && lineHeight*lines <= h {
   646  			lineY = (fieldHeight - lineHeight*i) - f.padding*2
   647  		}
   648  
   649  		// Draw line.
   650  		text.Draw(f.img, line, f.face, lineX, lineY, f.textColor)
   651  	}
   652  
   653  	return overflow
   654  }
   655  
   656  func (f *TextField) clampOffset() {
   657  	fieldSize := f.r.Dy()
   658  	if f.singleLine {
   659  		fieldSize = f.r.Dx()
   660  	}
   661  
   662  	if f.offset > f.bufferSize-fieldSize {
   663  		f.offset = f.bufferSize - fieldSize
   664  	}
   665  	if f.offset < 0 {
   666  		f.offset = 0
   667  	}
   668  }
   669  
   670  func (f *TextField) showScrollBar() bool {
   671  	return !f.singleLine && f.scrollVisible && (f.overflow || !f.scrollAutoHide)
   672  }
   673  
   674  // drawImage draws the field to img (caching it for future draws).
   675  func (f *TextField) drawImage() {
   676  	if rectIsZero(f.r) {
   677  		f.img = nil
   678  		return
   679  	}
   680  
   681  	w, h := f.r.Dx(), f.r.Dy()
   682  	var newImage bool
   683  	if f.img == nil {
   684  		newImage = true
   685  	} else {
   686  		imgRect := f.img.Bounds()
   687  		imgW, imgH := imgRect.Dx(), imgRect.Dy()
   688  		newImage = imgW != w || imgH != h
   689  	}
   690  	if newImage {
   691  		f.img = ebiten.NewImage(w, h)
   692  	}
   693  
   694  	f.wrapContent(false)
   695  	f.overflow = f.drawContent()
   696  	if f.showScrollBar() {
   697  		f.wrapContent(true)
   698  		f.drawContent()
   699  	}
   700  
   701  	// Draw scrollbar.
   702  	if f.showScrollBar() {
   703  		scrollAreaX, scrollAreaY := w-f.scrollWidth, 0
   704  		f.scrollRect = image.Rect(scrollAreaX, scrollAreaY, scrollAreaX+f.scrollWidth, h)
   705  
   706  		scrollBarH := f.scrollWidth / 2
   707  		if scrollBarH < 4 {
   708  			scrollBarH = 4
   709  		}
   710  
   711  		scrollX, scrollY := w-f.scrollWidth, 0
   712  		pct := float64(f.offset) / float64(f.bufferSize-h)
   713  		scrollY += int(float64(h-scrollBarH) * pct)
   714  		scrollBarRect := image.Rect(scrollX, scrollY, scrollX+f.scrollWidth, scrollY+scrollBarH)
   715  
   716  		f.img.SubImage(f.scrollRect).(*ebiten.Image).Fill(color.RGBA{200, 200, 200, 255})
   717  		f.img.SubImage(scrollBarRect).(*ebiten.Image).Fill(color.RGBA{108, 108, 108, 255})
   718  	}
   719  }
   720  
   721  func (f *TextField) bufferModified() {
   722  	f.drawImage()
   723  
   724  	if !f.follow {
   725  		return
   726  	}
   727  	fieldSize := f.r.Dy()
   728  	if f.singleLine {
   729  		fieldSize = f.r.Dx()
   730  	}
   731  	offset := f.bufferSize - fieldSize
   732  	if offset < 0 {
   733  		offset = 0
   734  	}
   735  	if offset != f.offset {
   736  		f.offset = offset
   737  		f.drawImage()
   738  	}
   739  }
   740  
   741  func rectIsZero(r image.Rectangle) bool {
   742  	return r == image.Rectangle{}
   743  }
   744  

View as plain text