...

Source file src/code.rocket9labs.com/tslocum/etk/messeji/textfield.go

Documentation: code.rocket9labs.com/tslocum/etk/messeji

     1  package messeji
     2  
     3  import (
     4  	"bytes"
     5  	"image"
     6  	"image/color"
     7  	"math"
     8  	"runtime/debug"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  	"unicode"
    13  
    14  	"github.com/hajimehoshi/ebiten/v2"
    15  	"github.com/hajimehoshi/ebiten/v2/inpututil"
    16  	"github.com/hajimehoshi/ebiten/v2/text"
    17  	"golang.org/x/image/font"
    18  	"golang.org/x/image/math/fixed"
    19  )
    20  
    21  // Alignment specifies how text is aligned within the field.
    22  type Alignment int
    23  
    24  const (
    25  	// AlignStart aligns text at the start of the field.
    26  	AlignStart Alignment = 0
    27  
    28  	// AlignCenter aligns text at the center of the field.
    29  	AlignCenter Alignment = 1
    30  
    31  	// AlignEnd aligns text at the end of the field.
    32  	AlignEnd Alignment = 2
    33  )
    34  
    35  const (
    36  	initialPadding     = 5
    37  	initialScrollWidth = 32
    38  )
    39  
    40  var (
    41  	initialForeground   = color.RGBA{0, 0, 0, 255}
    42  	initialBackground   = color.RGBA{255, 255, 255, 255}
    43  	initialScrollArea   = color.RGBA{200, 200, 200, 255}
    44  	initialScrollHandle = color.RGBA{108, 108, 108, 255}
    45  )
    46  
    47  // TextField is a text display field. Call Update and Draw when your Game's
    48  // Update and Draw methods are called.
    49  //
    50  // Note: A position and size must be set via SetRect before the field will appear.
    51  // Keyboard events are not handled by default, and may be enabled via SetHandleKeyboard.
    52  type TextField struct {
    53  	// r specifies the position and size of the field.
    54  	r image.Rectangle
    55  
    56  	// buffer is the text buffer split by newline characters.
    57  	buffer [][]byte
    58  
    59  	// incoming is text to be written to the buffer that has not yet been wrapped.
    60  	incoming []byte
    61  
    62  	// prefix is the text shown before the content of the field.
    63  	prefix string
    64  
    65  	// suffix is the text shown after the content of the field.
    66  	suffix string
    67  
    68  	// wordWrap determines whether content is wrapped at word boundaries.
    69  	wordWrap bool
    70  
    71  	// bufferWrapped is the content of the field after applying wrapping.
    72  	bufferWrapped []string
    73  
    74  	// wrapStart is the first line number in bufferWrapped which corresponds
    75  	// to the last line number in the actual text buffer.
    76  	wrapStart int
    77  
    78  	// needWrap is the first line number in the actual text buffer that needs to be wrapped.
    79  	needWrap int
    80  
    81  	// wrapScrollBar is whether the scroll bar was visible the last time the field was redrawn.
    82  	wrapScrollBar bool
    83  
    84  	// bufferSize is the size (in pixels) of the entire text buffer. When single
    85  	// line mode is enabled,
    86  	bufferSize int
    87  
    88  	// lineWidths is the size (in pixels) of each line as it appears on the screen.
    89  	lineWidths []int
    90  
    91  	// singleLine is whether the field displays all text on a single line.
    92  	singleLine bool
    93  
    94  	// horizontal is the horizontal alignment of the text within field.
    95  	horizontal Alignment
    96  
    97  	// vertical is the vertical alignment of the text within field.
    98  	vertical Alignment
    99  
   100  	// face is the font face of the text within the field.
   101  	face font.Face
   102  
   103  	// faceMutex is the lock which is held whenever utilizing the font face.
   104  	faceMutex *sync.Mutex
   105  
   106  	// lineHeight is the height of a single line of text.
   107  	lineHeight int
   108  
   109  	// overrideLineHeight is the custom height for a line of text, or 0 to disable.
   110  	overrideLineHeight int
   111  
   112  	// lineOffset is the offset of the baseline current font.
   113  	lineOffset int
   114  
   115  	// textColor is the color of the text within the field.
   116  	textColor color.RGBA
   117  
   118  	// backgroundColor is the color of the background of the field.
   119  	backgroundColor color.RGBA
   120  
   121  	// padding is the amount of padding around the text within the field.
   122  	padding int
   123  
   124  	// follow determines whether the field should automatically scroll to the
   125  	// end when content is added to the buffer.
   126  	follow bool
   127  
   128  	// overflow is whether the content of the field is currently larger than the field.
   129  	overflow bool
   130  
   131  	// offset is the current view offset of the text within the field, relative to the top.
   132  	offset int
   133  
   134  	// handleKeyboard is a flag which, when enabled, causes keyboard input to be handled.
   135  	handleKeyboard bool
   136  
   137  	// modified is a flag which, when enabled, causes bufferModified to be called
   138  	// during the next Draw call.
   139  	modified bool
   140  
   141  	// scrollRect specifies the position and size of the scrolling area.
   142  	scrollRect image.Rectangle
   143  
   144  	// scrollWidth is the width of the scroll bar.
   145  	scrollWidth int
   146  
   147  	// scrollAreaColor is the color of the scroll area.
   148  	scrollAreaColor color.RGBA
   149  
   150  	// scrollHandleColor is the color of the scroll handle.
   151  	scrollHandleColor color.RGBA
   152  
   153  	// scrollBorderSize is the size of the border around the scroll bar handle.
   154  	scrollBorderSize int
   155  
   156  	// Scroll bar handle border colors.
   157  	scrollBorderTop    color.RGBA
   158  	scrollBorderRight  color.RGBA
   159  	scrollBorderBottom color.RGBA
   160  	scrollBorderLeft   color.RGBA
   161  
   162  	// scrollVisible is whether the scroll bar is visible on the screen.
   163  	scrollVisible bool
   164  
   165  	// scrollAutoHide is whether the scroll bar should be automatically hidden
   166  	// when the entire text buffer fits within the screen.
   167  	scrollAutoHide bool
   168  
   169  	// scrollDrag is whether the scroll bar is currently being dragged.
   170  	scrollDrag bool
   171  
   172  	// img is the image of the field.
   173  	img *ebiten.Image
   174  
   175  	// visible is whether the field is visible on the screen.
   176  	visible bool
   177  
   178  	// redraw is whether the field needs to be redrawn.
   179  	redraw bool
   180  
   181  	// keyBuffer is a buffer of key press events.
   182  	keyBuffer []ebiten.Key
   183  
   184  	// keyBuffer is a buffer of runes from key presses.
   185  	runeBuffer []rune
   186  
   187  	sync.Mutex
   188  }
   189  
   190  // NewTextField returns a new TextField. See type documentation for more info.
   191  func NewTextField(face font.Face, faceMutex *sync.Mutex) *TextField {
   192  	if faceMutex == nil {
   193  		faceMutex = &sync.Mutex{}
   194  	}
   195  
   196  	f := &TextField{
   197  		face:              face,
   198  		faceMutex:         faceMutex,
   199  		textColor:         initialForeground,
   200  		backgroundColor:   initialBackground,
   201  		padding:           initialPadding,
   202  		scrollWidth:       initialScrollWidth,
   203  		scrollAreaColor:   initialScrollArea,
   204  		scrollHandleColor: initialScrollHandle,
   205  		follow:            true,
   206  		wordWrap:          true,
   207  		scrollVisible:     true,
   208  		scrollAutoHide:    true,
   209  		visible:           true,
   210  		redraw:            true,
   211  	}
   212  
   213  	f.faceMutex.Lock()
   214  	defer f.faceMutex.Unlock()
   215  
   216  	f.fontUpdated()
   217  	return f
   218  }
   219  
   220  // Rect returns the position and size of the field.
   221  func (f *TextField) Rect() image.Rectangle {
   222  	f.Lock()
   223  	defer f.Unlock()
   224  
   225  	return f.r
   226  }
   227  
   228  // SetRect sets the position and size of the field.
   229  func (f *TextField) SetRect(r image.Rectangle) {
   230  	f.Lock()
   231  	defer f.Unlock()
   232  
   233  	if f.r.Eq(r) {
   234  		return
   235  	}
   236  
   237  	if f.r.Dx() != r.Dx() || f.r.Dy() != r.Dy() {
   238  		f.bufferWrapped = f.bufferWrapped[:0]
   239  		f.lineWidths = f.lineWidths[:0]
   240  		f.needWrap = 0
   241  		f.wrapStart = 0
   242  		f.modified = true
   243  	}
   244  
   245  	f.r = r
   246  }
   247  
   248  // Text returns the text in the field.
   249  func (f *TextField) Text() string {
   250  	f.Lock()
   251  	defer f.Unlock()
   252  
   253  	f.processIncoming()
   254  
   255  	return string(bytes.Join(f.buffer, []byte("\n")))
   256  }
   257  
   258  // SetText sets the text in the field.
   259  func (f *TextField) SetText(text string) {
   260  	f.Lock()
   261  	defer f.Unlock()
   262  
   263  	f.buffer = f.buffer[:0]
   264  	f.bufferWrapped = f.bufferWrapped[:0]
   265  	f.lineWidths = f.lineWidths[:0]
   266  	f.needWrap = 0
   267  	f.wrapStart = 0
   268  	f.incoming = append(f.incoming[:0], []byte(text)...)
   269  	f.modified = true
   270  	f.redraw = true
   271  }
   272  
   273  // SetPrefix sets the text shown before the content of the field.
   274  func (f *TextField) SetPrefix(text string) {
   275  	f.Lock()
   276  	defer f.Unlock()
   277  
   278  	f.prefix = text
   279  	f.needWrap = 0
   280  	f.wrapStart = 0
   281  	f.modified = true
   282  }
   283  
   284  // SetSuffix sets the text shown after the content of the field.
   285  func (f *TextField) SetSuffix(text string) {
   286  	f.Lock()
   287  	defer f.Unlock()
   288  
   289  	f.suffix = text
   290  	f.needWrap = 0
   291  	f.wrapStart = 0
   292  	f.modified = true
   293  }
   294  
   295  // SetFollow sets whether the field should automatically scroll to the end when
   296  // content is added to the buffer.
   297  func (f *TextField) SetFollow(follow bool) {
   298  	f.Lock()
   299  	defer f.Unlock()
   300  
   301  	f.follow = follow
   302  }
   303  
   304  // SetSingleLine sets whether the field displays all text on a single line.
   305  // When enabled, the field scrolls horizontally. Otherwise, it scrolls vertically.
   306  func (f *TextField) SetSingleLine(single bool) {
   307  	f.Lock()
   308  	defer f.Unlock()
   309  
   310  	if f.singleLine == single {
   311  		return
   312  	}
   313  
   314  	f.singleLine = single
   315  	f.needWrap = 0
   316  	f.wrapStart = 0
   317  	f.modified = true
   318  }
   319  
   320  // SetHorizontal sets the horizontal alignment of the text within the field.
   321  func (f *TextField) SetHorizontal(h Alignment) {
   322  	f.Lock()
   323  	defer f.Unlock()
   324  
   325  	if f.horizontal == h {
   326  		return
   327  	}
   328  
   329  	f.horizontal = h
   330  	f.needWrap = 0
   331  	f.wrapStart = 0
   332  	f.modified = true
   333  }
   334  
   335  // SetVertical sets the veritcal alignment of the text within the field.
   336  func (f *TextField) SetVertical(v Alignment) {
   337  	f.Lock()
   338  	defer f.Unlock()
   339  
   340  	if f.vertical == v {
   341  		return
   342  	}
   343  
   344  	f.vertical = v
   345  	f.needWrap = 0
   346  	f.wrapStart = 0
   347  	f.modified = true
   348  }
   349  
   350  // LineHeight returns the line height for the field.
   351  func (f *TextField) LineHeight() int {
   352  	f.Lock()
   353  	defer f.Unlock()
   354  
   355  	if f.overrideLineHeight != 0 {
   356  		return f.overrideLineHeight
   357  	}
   358  	return f.lineHeight
   359  }
   360  
   361  // SetLineHeight sets a custom line height for the field. Setting a line
   362  // height of 0 restores the automatic line height detection based on the font.
   363  func (f *TextField) SetLineHeight(height int) {
   364  	f.Lock()
   365  	defer f.Unlock()
   366  
   367  	f.overrideLineHeight = height
   368  	f.needWrap = 0
   369  	f.wrapStart = 0
   370  	f.modified = true
   371  }
   372  
   373  // ForegroundColor returns the color of the text within the field.
   374  func (f *TextField) ForegroundColor() color.RGBA {
   375  	f.Lock()
   376  	defer f.Unlock()
   377  
   378  	return f.textColor
   379  }
   380  
   381  // SetForegroundColor sets the color of the text within the field.
   382  func (f *TextField) SetForegroundColor(c color.RGBA) {
   383  	f.Lock()
   384  	defer f.Unlock()
   385  
   386  	f.textColor = c
   387  	f.modified = true
   388  }
   389  
   390  // SetBackgroundColor sets the color of the background of the field.
   391  func (f *TextField) SetBackgroundColor(c color.RGBA) {
   392  	f.Lock()
   393  	defer f.Unlock()
   394  
   395  	f.backgroundColor = c
   396  	f.modified = true
   397  }
   398  
   399  // SetFont sets the font face of the text within the field.
   400  func (f *TextField) SetFont(face font.Face, mutex *sync.Mutex) {
   401  	if mutex == nil {
   402  		mutex = &sync.Mutex{}
   403  	}
   404  
   405  	f.Lock()
   406  	defer f.Unlock()
   407  
   408  	mutex.Lock()
   409  	defer mutex.Unlock()
   410  
   411  	f.face = face
   412  	f.faceMutex = mutex
   413  	f.fontUpdated()
   414  
   415  	f.needWrap = 0
   416  	f.wrapStart = 0
   417  	f.modified = true
   418  }
   419  
   420  // Padding returns the amount of padding around the text within the field.
   421  func (f *TextField) Padding() int {
   422  	f.Lock()
   423  	defer f.Unlock()
   424  
   425  	return f.padding
   426  }
   427  
   428  // SetPadding sets the amount of padding around the text within the field.
   429  func (f *TextField) SetPadding(padding int) {
   430  	f.Lock()
   431  	defer f.Unlock()
   432  
   433  	f.padding = padding
   434  	f.needWrap = 0
   435  	f.wrapStart = 0
   436  	f.modified = true
   437  }
   438  
   439  // Visible returns whether the field is currently visible on the screen.
   440  func (f *TextField) Visible() bool {
   441  	return f.visible
   442  }
   443  
   444  // SetVisible sets whether the field is visible on the screen.
   445  func (f *TextField) SetVisible(visible bool) {
   446  	f.Lock()
   447  	defer f.Unlock()
   448  
   449  	if f.visible == visible {
   450  		return
   451  	}
   452  
   453  	f.visible = visible
   454  	if visible {
   455  		f.redraw = true
   456  	}
   457  }
   458  
   459  // SetScrollBarWidth sets the width of the scroll bar.
   460  func (f *TextField) SetScrollBarWidth(width int) {
   461  	f.Lock()
   462  	defer f.Unlock()
   463  
   464  	if f.scrollWidth == width {
   465  		return
   466  	}
   467  
   468  	f.scrollWidth = width
   469  	f.needWrap = 0
   470  	f.wrapStart = 0
   471  	f.modified = true
   472  }
   473  
   474  // SetScrollBarColors sets the color of the scroll bar area and handle.
   475  func (f *TextField) SetScrollBarColors(area color.RGBA, handle color.RGBA) {
   476  	f.Lock()
   477  	defer f.Unlock()
   478  
   479  	f.scrollAreaColor, f.scrollHandleColor = area, handle
   480  	f.redraw = true
   481  }
   482  
   483  // SetScrollBorderSize sets the size of the border around the scroll bar handle.
   484  func (f *TextField) SetScrollBorderSize(size int) {
   485  	f.Lock()
   486  	defer f.Unlock()
   487  
   488  	f.scrollBorderSize = size
   489  	f.redraw = true
   490  }
   491  
   492  // SetScrollBorderColor sets the color of the top, right, bottom and left border
   493  // of the scroll bar handle.
   494  func (f *TextField) SetScrollBorderColors(top color.RGBA, right color.RGBA, bottom color.RGBA, left color.RGBA) {
   495  	f.Lock()
   496  	defer f.Unlock()
   497  
   498  	f.scrollBorderTop = top
   499  	f.scrollBorderRight = right
   500  	f.scrollBorderBottom = bottom
   501  	f.scrollBorderLeft = left
   502  	f.redraw = true
   503  }
   504  
   505  // SetScrollBarVisible sets whether the scroll bar is visible on the screen.
   506  func (f *TextField) SetScrollBarVisible(scrollVisible bool) {
   507  	f.Lock()
   508  	defer f.Unlock()
   509  
   510  	if f.scrollVisible == scrollVisible {
   511  		return
   512  	}
   513  
   514  	f.scrollVisible = scrollVisible
   515  	f.needWrap = 0
   516  	f.wrapStart = 0
   517  	f.modified = true
   518  }
   519  
   520  // SetAutoHideScrollBar sets whether the scroll bar is automatically hidden
   521  // when the entire text buffer is visible.
   522  func (f *TextField) SetAutoHideScrollBar(autoHide bool) {
   523  	f.Lock()
   524  	defer f.Unlock()
   525  
   526  	if f.scrollAutoHide == autoHide {
   527  		return
   528  	}
   529  
   530  	f.scrollAutoHide = autoHide
   531  	f.needWrap = 0
   532  	f.wrapStart = 0
   533  	f.modified = true
   534  }
   535  
   536  // WordWrap returns the current text wrap mode.
   537  func (f *TextField) WordWrap() bool {
   538  	f.Lock()
   539  	defer f.Unlock()
   540  
   541  	return f.wordWrap
   542  }
   543  
   544  // SetWordWrap sets a flag which, when enabled, causes text to wrap without breaking words.
   545  func (f *TextField) SetWordWrap(wrap bool) {
   546  	f.Lock()
   547  	defer f.Unlock()
   548  
   549  	if f.wordWrap == wrap {
   550  		return
   551  	}
   552  
   553  	f.wordWrap = wrap
   554  	f.needWrap = 0
   555  	f.wrapStart = 0
   556  	f.modified = true
   557  }
   558  
   559  // SetHandleKeyboard sets a flag controlling whether keyboard input should be handled
   560  // by the field. This can be used to facilitate focus changes between multiple inputs.
   561  func (f *TextField) SetHandleKeyboard(handle bool) {
   562  	f.Lock()
   563  	defer f.Unlock()
   564  
   565  	f.handleKeyboard = handle
   566  }
   567  
   568  // Write writes to the field's buffer.
   569  func (f *TextField) Write(p []byte) (n int, err error) {
   570  	f.Lock()
   571  	defer f.Unlock()
   572  
   573  	return f._write(p)
   574  }
   575  
   576  func (f *TextField) _write(p []byte) (n int, err error) {
   577  	f.incoming = append(f.incoming, p...)
   578  	f.modified = true
   579  	f.redraw = true
   580  	return len(p), nil
   581  }
   582  
   583  // HandleKeyboardEvent passes the provided key or rune to the TextField.
   584  func (f *TextField) HandleKeyboardEvent(key ebiten.Key, r rune) (handled bool, err error) {
   585  	f.Lock()
   586  	defer f.Unlock()
   587  
   588  	if !f.visible || rectIsZero(f.r) || !f.handleKeyboard {
   589  		return false, nil
   590  	}
   591  
   592  	return f._handleKeyboardEvent(key, r)
   593  }
   594  
   595  func (f *TextField) _handleKeyboardEvent(key ebiten.Key, r rune) (handled bool, err error) {
   596  	if key != -1 {
   597  		// Handle keyboard PageUp/PageDown.
   598  		offsetAmount := 0
   599  		switch key {
   600  		case ebiten.KeyPageUp:
   601  			offsetAmount = 100
   602  		case ebiten.KeyPageDown:
   603  			offsetAmount = -100
   604  		}
   605  		if offsetAmount != 0 {
   606  			f.offset += offsetAmount
   607  			f.clampOffset()
   608  			f.redraw = true
   609  			return true, nil
   610  		}
   611  		return true, err
   612  	}
   613  	return true, nil
   614  }
   615  
   616  func (f *TextField) HandleMouseEvent(cursor image.Point, pressed bool, clicked bool) (handled bool, err error) {
   617  	f.Lock()
   618  	defer f.Unlock()
   619  
   620  	if !f.visible || rectIsZero(f.r) {
   621  		return false, nil
   622  	}
   623  
   624  	return f._handleMouseEvent(cursor, pressed, clicked)
   625  }
   626  
   627  func (f *TextField) _handleMouseEvent(cursor image.Point, pressed bool, clicked bool) (handled bool, err error) {
   628  	if !cursor.In(f.r) {
   629  		return false, nil
   630  	}
   631  
   632  	// Handle mouse wheel.
   633  	_, scrollY := ebiten.Wheel()
   634  	if scrollY != 0 {
   635  		const offsetAmount = 25
   636  		f.offset += int(scrollY * offsetAmount)
   637  		f.clampOffset()
   638  		f.redraw = true
   639  	}
   640  
   641  	// Handle scroll bar click (and drag).
   642  	if !f.showScrollBar() {
   643  		return true, nil
   644  	} else if pressed || f.scrollDrag {
   645  		p := image.Point{cursor.X - f.r.Min.X, cursor.Y - f.r.Min.Y}
   646  		if pressed && p.In(f.scrollRect) {
   647  			dragY := cursor.Y - f.r.Min.Y - f.scrollWidth/4
   648  			if dragY < 0 {
   649  				dragY = 0
   650  			} else if dragY > f.scrollRect.Dy() {
   651  				dragY = f.scrollRect.Dy()
   652  			}
   653  
   654  			pct := float64(dragY) / float64(f.scrollRect.Dy()-f.scrollWidth/2)
   655  			if pct < 0 {
   656  				pct = 0
   657  			} else if pct > 1 {
   658  				pct = 1
   659  			}
   660  
   661  			h := f.r.Dy()
   662  			f.offset = -int(float64(f.bufferSize-h-f.lineOffset+f.padding*2) * pct)
   663  			f.clampOffset()
   664  
   665  			f.redraw = true
   666  			f.scrollDrag = true
   667  		} else if !pressed {
   668  			f.scrollDrag = false
   669  		}
   670  	}
   671  	return true, nil
   672  }
   673  
   674  // Update updates the field. This function should be called when
   675  // Game.Update is called.
   676  func (f *TextField) Update() error {
   677  	f.Lock()
   678  	defer f.Unlock()
   679  
   680  	if !f.visible || rectIsZero(f.r) {
   681  		return nil
   682  	}
   683  
   684  	f.keyBuffer = inpututil.AppendJustPressedKeys(f.keyBuffer[:0])
   685  	for _, key := range f.keyBuffer {
   686  		handled, err := f._handleKeyboardEvent(key, 0)
   687  		if err != nil {
   688  			return err
   689  		} else if handled {
   690  			f.redraw = true
   691  		}
   692  	}
   693  
   694  	f.runeBuffer = ebiten.AppendInputChars(f.runeBuffer[:0])
   695  	for _, r := range f.runeBuffer {
   696  		handled, err := f._handleKeyboardEvent(-1, r)
   697  		if err != nil {
   698  			return err
   699  		} else if handled {
   700  			f.redraw = true
   701  		}
   702  	}
   703  
   704  	cx, cy := ebiten.CursorPosition()
   705  	if cx != 0 || cy != 0 {
   706  		handled, err := f._handleMouseEvent(image.Point{X: cx, Y: cy}, ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft), inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft))
   707  		if err != nil {
   708  			return err
   709  		} else if handled {
   710  			f.redraw = true
   711  		}
   712  	}
   713  
   714  	return nil
   715  }
   716  
   717  // Draw draws the field on the screen. This function should be called
   718  // when Game.Draw is called.
   719  func (f *TextField) Draw(screen *ebiten.Image) {
   720  	f.Lock()
   721  	defer f.Unlock()
   722  
   723  	if f.modified {
   724  		f.faceMutex.Lock()
   725  
   726  		f.bufferModified()
   727  		f.modified = false
   728  
   729  		f.faceMutex.Unlock()
   730  	}
   731  
   732  	if !f.visible || rectIsZero(f.r) {
   733  		return
   734  	}
   735  
   736  	if f.redraw {
   737  		f.faceMutex.Lock()
   738  
   739  		f.drawImage()
   740  		f.redraw = false
   741  
   742  		f.faceMutex.Unlock()
   743  	}
   744  
   745  	op := &ebiten.DrawImageOptions{}
   746  	op.GeoM.Translate(float64(f.r.Min.X), float64(f.r.Min.Y))
   747  	screen.DrawImage(f.img, op)
   748  }
   749  
   750  func (f *TextField) fontUpdated() {
   751  	m := f.face.Metrics()
   752  	f.lineHeight = m.Height.Ceil()
   753  	f.lineOffset = m.CapHeight.Ceil()
   754  	if f.lineOffset < 0 {
   755  		f.lineOffset *= -1
   756  	}
   757  }
   758  
   759  func (f *TextField) wrapContent(withScrollBar bool) {
   760  	if withScrollBar != f.wrapScrollBar {
   761  		f.needWrap = 0
   762  		f.wrapStart = 0
   763  	} else if f.needWrap == -1 {
   764  		return
   765  	}
   766  	f.wrapScrollBar = withScrollBar
   767  
   768  	if f.singleLine || len(f.buffer) == 0 {
   769  		buffer := f.prefix + string(bytes.Join(f.buffer, nil)) + f.suffix
   770  		bounds, _ := boundString(f.face, buffer)
   771  
   772  		f.bufferWrapped = []string{buffer}
   773  		f.wrapStart = 0
   774  		f.lineWidths = append(f.lineWidths[:0], (bounds.Max.X - bounds.Min.X).Floor())
   775  
   776  		f.needWrap = -1
   777  		return
   778  	}
   779  
   780  	w := f.r.Dx()
   781  	if withScrollBar {
   782  		w -= f.scrollWidth
   783  	}
   784  	bufferLen := len(f.buffer)
   785  	j := f.wrapStart
   786  	for i := f.needWrap; i < bufferLen; i++ {
   787  		var line string
   788  		if i == 0 {
   789  			line = f.prefix + string(f.buffer[i])
   790  		} else {
   791  			line = string(f.buffer[i])
   792  		}
   793  		if i == bufferLen-1 {
   794  			line += f.suffix
   795  		}
   796  		l := len(line)
   797  		availableWidth := w - (f.padding * 2)
   798  
   799  		f.wrapStart = j
   800  
   801  		// BoundString returns 0 for strings containing only whitespace.
   802  		if strings.TrimSpace(line) == "" {
   803  			if len(f.bufferWrapped) <= j {
   804  				f.bufferWrapped = append(f.bufferWrapped, "")
   805  			} else {
   806  				f.bufferWrapped[j] = ""
   807  			}
   808  			if len(f.lineWidths) <= j {
   809  				f.lineWidths = append(f.lineWidths, 0)
   810  			} else {
   811  				f.lineWidths[j] = 0
   812  			}
   813  			j++
   814  			continue
   815  		}
   816  
   817  		var start int
   818  		var end int
   819  		for start < l {
   820  			end = l
   821  			var initialEnd int
   822  			var bounds fixed.Rectangle26_6
   823  			var boundsWidth int
   824  
   825  			// Chop the line in half until it fits.
   826  			for end > start {
   827  				initialEnd = end
   828  
   829  				bounds, _ := boundString(f.face, line[start:end])
   830  				boundsWidth := (bounds.Max.X - bounds.Min.X).Floor()
   831  				if boundsWidth > availableWidth && end > start+1 {
   832  					delta := (end - start) / 2
   833  					if delta < 1 {
   834  						delta = 1
   835  					}
   836  					end -= delta
   837  				} else {
   838  					break
   839  				}
   840  			}
   841  
   842  			// Add characters until the line doesn't fit anymore.
   843  			lineEnd := end
   844  			var lastSpace = -1
   845  			for end < l {
   846  				initialEnd = end
   847  
   848  				bounds, _ := boundString(f.face, line[start:end])
   849  				boundsWidth := (bounds.Max.X - bounds.Min.X).Floor()
   850  				if boundsWidth > availableWidth && end > start+1 {
   851  					break
   852  				}
   853  
   854  				lineEnd = end
   855  				end++
   856  				if unicode.IsSpace(rune(line[lineEnd])) {
   857  					lastSpace = lineEnd
   858  				}
   859  			}
   860  
   861  			// Apply word wrapping.
   862  			if f.wordWrap && lineEnd < l {
   863  				if lastSpace == -1 {
   864  					// Search for a space going backwards.
   865  					end = lineEnd
   866  					for offset := 1; offset < end-start-2; offset++ {
   867  						if unicode.IsSpace(rune(line[end-offset])) {
   868  							lastSpace = end - offset
   869  							break
   870  						}
   871  					}
   872  				}
   873  				if lastSpace != -1 {
   874  					end = lastSpace + 1
   875  				} else {
   876  					end = lineEnd
   877  				}
   878  			} else {
   879  				end = lineEnd
   880  			}
   881  
   882  			if boundsWidth == 0 || end != initialEnd {
   883  				bounds, _ = boundString(f.face, line[start:end])
   884  				boundsWidth = (bounds.Max.X - bounds.Min.X).Floor()
   885  			}
   886  
   887  			if len(f.bufferWrapped) <= j {
   888  				f.bufferWrapped = append(f.bufferWrapped, line[start:end])
   889  			} else {
   890  				f.bufferWrapped[j] = line[start:end]
   891  			}
   892  			if len(f.lineWidths) <= j {
   893  				f.lineWidths = append(f.lineWidths, boundsWidth)
   894  			} else {
   895  				f.lineWidths[j] = boundsWidth
   896  			}
   897  			j++
   898  
   899  			start = end
   900  		}
   901  	}
   902  
   903  	if len(f.bufferWrapped) >= j {
   904  		f.bufferWrapped = f.bufferWrapped[:j]
   905  	}
   906  
   907  	f.needWrap = -1
   908  }
   909  
   910  // drawContent draws the text buffer to img.
   911  func (f *TextField) drawContent() (overflow bool) {
   912  	if f.backgroundColor.A != 0 {
   913  		f.img.Fill(f.backgroundColor)
   914  	} else {
   915  		f.img.Clear()
   916  	}
   917  	fieldWidth := f.r.Dx()
   918  	fieldHeight := f.r.Dy()
   919  	if f.showScrollBar() {
   920  		fieldWidth -= f.scrollWidth
   921  	}
   922  	lines := len(f.bufferWrapped)
   923  
   924  	h := f.r.Dy()
   925  	lineHeight := f.overrideLineHeight
   926  	if lineHeight == 0 {
   927  		lineHeight = f.lineHeight
   928  	}
   929  	var firstVisible, lastVisible int
   930  	firstVisible = 0
   931  	lastVisible = len(f.bufferWrapped) - 1
   932  	if !f.singleLine {
   933  		firstVisible = (f.offset * -1) / f.lineHeight
   934  		lastVisible = firstVisible + (f.r.Dy() / f.lineHeight) + 1
   935  		if lastVisible > len(f.bufferWrapped)-1 {
   936  			lastVisible = len(f.bufferWrapped) - 1
   937  		}
   938  	}
   939  	// Calculate buffer size (width for single-line fields or height for multi-line fields).
   940  	if f.singleLine {
   941  		bounds, _ := boundString(f.face, f.bufferWrapped[firstVisible])
   942  		f.bufferSize = (bounds.Max.X - bounds.Min.X).Floor()
   943  	} else {
   944  		f.bufferSize = (len(f.bufferWrapped)) * lineHeight
   945  	}
   946  	for i := firstVisible; i <= lastVisible; i++ {
   947  		line := f.bufferWrapped[i]
   948  		lineX := f.padding
   949  		lineY := 1 + f.padding + f.lineOffset + lineHeight*i
   950  
   951  		// Calculate whether the line overflows the visible area.
   952  		lineOverflows := lineY < 0 || lineY >= h-(f.padding*2)
   953  		if lineOverflows {
   954  			overflow = true
   955  		}
   956  
   957  		// Skip drawing off-screen lines.
   958  		if lineY < 0 {
   959  			continue
   960  		}
   961  
   962  		// Apply scrolling transformation.
   963  		if f.singleLine {
   964  			lineX += f.offset
   965  		} else {
   966  			lineY += f.offset
   967  		}
   968  
   969  		// Align horizontally.
   970  		if f.horizontal == AlignCenter {
   971  			lineX = (fieldWidth - f.lineWidths[i]) / 2
   972  		} else if f.horizontal == AlignEnd {
   973  			lineX = (fieldWidth - f.lineWidths[i]) - f.padding - 1
   974  		}
   975  
   976  		// Align vertically.
   977  		totalHeight := f.lineOffset + lineHeight*(lines-1)
   978  		if f.vertical == AlignCenter && totalHeight <= h {
   979  			lineY = fieldHeight/2 - totalHeight/2 + f.lineOffset + (lineHeight * (i))
   980  		} else if f.vertical == AlignEnd && totalHeight <= h {
   981  			lineY = (fieldHeight - lineHeight*i) - f.padding
   982  		}
   983  
   984  		// Draw line.
   985  		text.Draw(f.img, line, f.face, lineX, lineY, f.textColor)
   986  	}
   987  
   988  	return overflow
   989  }
   990  
   991  func (f *TextField) clampOffset() {
   992  	fieldSize := f.r.Dy()
   993  	if f.singleLine {
   994  		fieldSize = f.r.Dx()
   995  	}
   996  	minSize := -(f.bufferSize - fieldSize + f.padding*2)
   997  	if !f.singleLine {
   998  		minSize += f.lineOffset
   999  	}
  1000  	if minSize > 0 {
  1001  		minSize = 0
  1002  	}
  1003  	maxSize := 0
  1004  	if f.offset < minSize {
  1005  		f.offset = minSize
  1006  	} else if f.offset > maxSize {
  1007  		f.offset = maxSize
  1008  	}
  1009  }
  1010  
  1011  func (f *TextField) showScrollBar() bool {
  1012  	return !f.singleLine && f.scrollVisible && (f.overflow || !f.scrollAutoHide)
  1013  }
  1014  
  1015  func (f *TextField) wrap() {
  1016  	showScrollBar := f.showScrollBar()
  1017  	f.wrapContent(showScrollBar)
  1018  	f.overflow = f.drawContent()
  1019  	if f.showScrollBar() != showScrollBar {
  1020  		f.wrapContent(!showScrollBar)
  1021  		f.drawContent()
  1022  	}
  1023  }
  1024  
  1025  // drawImage draws the field to img (caching it for future draws).
  1026  func (f *TextField) drawImage() {
  1027  	if rectIsZero(f.r) {
  1028  		f.img = nil
  1029  		return
  1030  	}
  1031  
  1032  	w, h := f.r.Dx(), f.r.Dy()
  1033  
  1034  	var newImage bool
  1035  	if f.img == nil {
  1036  		newImage = true
  1037  	} else {
  1038  		imgRect := f.img.Bounds()
  1039  		imgW, imgH := imgRect.Dx(), imgRect.Dy()
  1040  		newImage = imgW != w || imgH != h
  1041  	}
  1042  	if newImage {
  1043  		f.img = ebiten.NewImage(w, h)
  1044  	}
  1045  
  1046  	f.wrap()
  1047  
  1048  	// Draw scrollbar.
  1049  	if f.showScrollBar() {
  1050  		scrollAreaX, scrollAreaY := w-f.scrollWidth, 0
  1051  		f.scrollRect = image.Rect(scrollAreaX, scrollAreaY, scrollAreaX+f.scrollWidth, h)
  1052  
  1053  		scrollBarH := f.scrollWidth / 2
  1054  		if scrollBarH < 4 {
  1055  			scrollBarH = 4
  1056  		}
  1057  
  1058  		scrollX, scrollY := w-f.scrollWidth, 0
  1059  		pct := float64(-f.offset) / float64(f.bufferSize-h-f.lineOffset+f.padding*2)
  1060  		scrollY += int(float64(h-scrollBarH) * pct)
  1061  		scrollBarRect := image.Rect(scrollX, scrollY, scrollX+f.scrollWidth, scrollY+scrollBarH)
  1062  
  1063  		// Draw scroll area.
  1064  		f.img.SubImage(f.scrollRect).(*ebiten.Image).Fill(f.scrollAreaColor)
  1065  
  1066  		// Draw scroll handle.
  1067  		f.img.SubImage(scrollBarRect).(*ebiten.Image).Fill(f.scrollHandleColor)
  1068  
  1069  		// Draw scroll handle border.
  1070  		if f.scrollBorderSize != 0 {
  1071  			r := scrollBarRect
  1072  			f.img.SubImage(image.Rect(r.Min.X, r.Min.Y, r.Min.X+f.scrollBorderSize, r.Max.Y)).(*ebiten.Image).Fill(f.scrollBorderLeft)
  1073  			f.img.SubImage(image.Rect(r.Min.X, r.Min.Y, r.Max.X, r.Min.Y+f.scrollBorderSize)).(*ebiten.Image).Fill(f.scrollBorderTop)
  1074  			f.img.SubImage(image.Rect(r.Max.X-f.scrollBorderSize, r.Min.Y, r.Max.X, r.Max.Y)).(*ebiten.Image).Fill(f.scrollBorderRight)
  1075  			f.img.SubImage(image.Rect(r.Min.X, r.Max.Y-f.scrollBorderSize, r.Max.X, r.Max.Y)).(*ebiten.Image).Fill(f.scrollBorderBottom)
  1076  		}
  1077  	}
  1078  }
  1079  
  1080  func (f *TextField) processIncoming() {
  1081  	if len(f.incoming) == 0 {
  1082  		return
  1083  	}
  1084  
  1085  	line := len(f.buffer) - 1
  1086  	if line < 0 {
  1087  		line = 0
  1088  		f.buffer = append(f.buffer, nil)
  1089  	}
  1090  	if f.needWrap == -1 {
  1091  		f.needWrap = line
  1092  	}
  1093  	for _, b := range f.incoming {
  1094  		if b == '\n' {
  1095  			line++
  1096  			f.buffer = append(f.buffer, nil)
  1097  			continue
  1098  		}
  1099  		f.buffer[line] = append(f.buffer[line], b)
  1100  	}
  1101  	f.incoming = f.incoming[:0]
  1102  }
  1103  
  1104  func (f *TextField) bufferModified() {
  1105  	f.processIncoming()
  1106  
  1107  	f.drawImage()
  1108  
  1109  	lastOffset := f.offset
  1110  	if f.follow {
  1111  		f.offset = -math.MaxInt
  1112  		f.clampOffset()
  1113  		if f.offset != lastOffset {
  1114  			f.drawImage()
  1115  		}
  1116  	}
  1117  
  1118  	f.redraw = false
  1119  }
  1120  
  1121  func rectIsZero(r image.Rectangle) bool {
  1122  	return r.Dx() == 0 || r.Dy() == 0
  1123  }
  1124  
  1125  func boundString(f font.Face, s string) (bounds fixed.Rectangle26_6, advance fixed.Int26_6) {
  1126  	if strings.TrimSpace(s) == "" {
  1127  		return fixed.Rectangle26_6{}, 0
  1128  	}
  1129  	for i := 0; i < 100; i++ {
  1130  		bounds, advance = func() (fixed.Rectangle26_6, fixed.Int26_6) {
  1131  			defer func() {
  1132  				err := recover()
  1133  				if err != nil && i == 99 {
  1134  					debug.PrintStack()
  1135  					panic("failed to calculate bounds of string '" + s + "'")
  1136  				}
  1137  			}()
  1138  			bounds, advance = font.BoundString(f, s)
  1139  			return bounds, advance
  1140  		}()
  1141  		if !bounds.Empty() {
  1142  			return bounds, advance
  1143  		}
  1144  		time.Sleep(10 * time.Millisecond)
  1145  	}
  1146  	return fixed.Rectangle26_6{}, 0
  1147  }
  1148  

View as plain text