github.com/gop9/olt@v0.0.0-20200202132135-d956aad50b08/framework/text.go (about)

     1  package framework
     2  
     3  import (
     4  	"image"
     5  	"image/color"
     6  	"math"
     7  	"runtime"
     8  	"time"
     9  	"unicode"
    10  
    11  	gkey "github.com/gop9/olt/gio/io/key"
    12  
    13  	"github.com/gop9/olt/framework/clipboard"
    14  	"github.com/gop9/olt/framework/command"
    15  	"github.com/gop9/olt/framework/font"
    16  	"github.com/gop9/olt/framework/label"
    17  	"github.com/gop9/olt/framework/rect"
    18  	nstyle "github.com/gop9/olt/framework/style"
    19  	"github.com/gop9/olt/gio/io/pointer"
    20  )
    21  
    22  ///////////////////////////////////////////////////////////////////////////////////
    23  // TEXT EDITOR
    24  ///////////////////////////////////////////////////////////////////////////////////
    25  
    26  type propertyStatus int
    27  
    28  const (
    29  	propertyDefault = propertyStatus(iota)
    30  	propertyEdit
    31  	propertyDrag
    32  )
    33  
    34  // TextEditor stores the state of a text editor.
    35  // To add a text editor to a window create a TextEditor object with
    36  // &TextEditor{}, store it somewhere then in the update function call
    37  // the Edit method passing the window to it.
    38  type TextEditor struct {
    39  	win            *Window
    40  	propertyStatus propertyStatus
    41  	Cursor         int
    42  	Buffer         []rune
    43  	Filter         FilterFunc
    44  	Flags          EditFlags
    45  	CursorFollow   bool
    46  	Redraw         bool
    47  
    48  	Maxlen int
    49  
    50  	PasswordChar rune // if non-zero all characters are displayed like this character
    51  
    52  	Initialized            bool
    53  	Active                 bool
    54  	InsertMode             bool
    55  	Scrollbar              image.Point
    56  	SelectStart, SelectEnd int
    57  	HasPreferredX          bool
    58  	SingleLine             bool
    59  	PreferredX             int
    60  	Undo                   textUndoState
    61  
    62  	drawchunks []drawchunk
    63  
    64  	lastClickCoord  image.Point
    65  	lastClickTime   time.Time
    66  	clickCount      int
    67  	trueSelectStart int
    68  
    69  	needle []rune
    70  
    71  	password []rune // support buffer for drawing PasswordChar!=0 fields
    72  }
    73  
    74  type drawchunk struct {
    75  	rect.Rect
    76  	start, end int
    77  }
    78  
    79  func (ed *TextEditor) init(win *Window) {
    80  	if ed.Filter == nil {
    81  		ed.Filter = FilterDefault
    82  	}
    83  	if !ed.Initialized {
    84  		if ed.Flags&EditMultiline != 0 {
    85  			ed.clearState(TextEditMultiLine)
    86  		} else {
    87  			ed.clearState(TextEditSingleLine)
    88  		}
    89  
    90  	}
    91  	if ed.win == nil || ed.win != win {
    92  		if ed.win == nil {
    93  			if ed.Buffer == nil {
    94  				ed.Buffer = []rune{}
    95  			}
    96  			ed.Filter = nil
    97  			ed.Cursor = 0
    98  		}
    99  		ed.Redraw = true
   100  		ed.win = win
   101  	}
   102  }
   103  
   104  type EditFlags int
   105  
   106  const (
   107  	EditDefault  EditFlags = 0
   108  	EditReadOnly EditFlags = 1 << iota
   109  	EditAutoSelect
   110  	EditSigEnter
   111  	EditNoCursor
   112  	EditSelectable
   113  	EditClipboard
   114  	EditCtrlEnterNewline
   115  	EditNoHorizontalScroll
   116  	EditAlwaysInsertMode
   117  	EditMultiline
   118  	EditNeverInsertMode
   119  	EditFocusFollowsMouse
   120  	EditNoContextMenu
   121  	EditIbeamCursor
   122  
   123  	EditSimple = EditAlwaysInsertMode
   124  	EditField  = EditSelectable | EditClipboard | EditSigEnter
   125  	EditBox    = EditSelectable | EditMultiline | EditClipboard
   126  )
   127  
   128  type EditEvents int
   129  
   130  const (
   131  	EditActive EditEvents = 1 << iota
   132  	EditInactive
   133  	EditActivated
   134  	EditDeactivated
   135  	EditCommitted
   136  )
   137  
   138  type TextEditType int
   139  
   140  const (
   141  	TextEditSingleLine TextEditType = iota
   142  	TextEditMultiLine
   143  )
   144  
   145  type textUndoRecord struct {
   146  	Where        int
   147  	InsertLength int
   148  	DeleteLength int
   149  	Text         []rune
   150  }
   151  
   152  const _TEXTEDIT_UNDOSTATECOUNT = 99
   153  
   154  type textUndoState struct {
   155  	UndoRec   [_TEXTEDIT_UNDOSTATECOUNT]textUndoRecord
   156  	UndoPoint int16
   157  	RedoPoint int16
   158  }
   159  
   160  func strInsertText(str []rune, pos int, runes []rune) []rune {
   161  	if cap(str) < len(str)+len(runes) {
   162  		newcap := (cap(str) + 1) * 2
   163  		if newcap < len(str)+len(runes) {
   164  			newcap = len(str) + len(runes)
   165  		}
   166  		newstr := make([]rune, len(str), newcap)
   167  		copy(newstr, str)
   168  		str = newstr
   169  	}
   170  	str = str[:len(str)+len(runes)]
   171  	copy(str[pos+len(runes):], str[pos:])
   172  	copy(str[pos:], runes)
   173  	return str
   174  }
   175  
   176  func strDeleteText(s []rune, pos int, dlen int) []rune {
   177  	copy(s[pos:], s[pos+dlen:])
   178  	s = s[:len(s)-dlen]
   179  	return s
   180  }
   181  
   182  func (s *TextEditor) hasSelection() bool {
   183  	return s.SelectStart != s.SelectEnd
   184  }
   185  
   186  func (edit *TextEditor) locateCoord(p image.Point, font font.Face, row_height int) int {
   187  	x, y := p.X, p.Y
   188  
   189  	var drawchunk *drawchunk
   190  
   191  	for i := range edit.drawchunks {
   192  		min := edit.drawchunks[i].Min()
   193  		max := edit.drawchunks[i].Max()
   194  		getprev := false
   195  		if min.Y <= y && y <= max.Y {
   196  			if min.X <= x && x <= max.X {
   197  				drawchunk = &edit.drawchunks[i]
   198  			}
   199  			if min.X > x {
   200  				getprev = true
   201  			}
   202  		} else if min.Y > y {
   203  			getprev = true
   204  		}
   205  		if getprev {
   206  			if i == 0 {
   207  				drawchunk = &edit.drawchunks[0]
   208  			} else {
   209  				drawchunk = &edit.drawchunks[i-1]
   210  			}
   211  			break
   212  		}
   213  	}
   214  
   215  	if drawchunk == nil {
   216  		return len(edit.Buffer)
   217  	}
   218  
   219  	curx := drawchunk.X
   220  	for i := drawchunk.start; i < drawchunk.end && i < len(edit.Buffer); i++ {
   221  		curx += FontWidth(font, string(edit.Buffer[i:i+1]))
   222  		if curx > x {
   223  			return i
   224  		}
   225  	}
   226  
   227  	if drawchunk.end >= len(edit.Buffer) {
   228  		return len(edit.Buffer)
   229  	}
   230  
   231  	return drawchunk.end
   232  }
   233  
   234  func (edit *TextEditor) indexToCoord(index int, font font.Face, row_height int) image.Point {
   235  	var drawchunk *drawchunk
   236  
   237  	for i := range edit.drawchunks {
   238  		if edit.drawchunks[i].start > index {
   239  			if i == 0 {
   240  				drawchunk = &edit.drawchunks[0]
   241  			} else {
   242  				drawchunk = &edit.drawchunks[i-1]
   243  			}
   244  			break
   245  		}
   246  	}
   247  
   248  	if drawchunk == nil {
   249  		if len(edit.drawchunks) == 0 {
   250  			return image.Point{}
   251  		}
   252  		drawchunk = &edit.drawchunks[len(edit.drawchunks)-1]
   253  	}
   254  
   255  	x := drawchunk.X
   256  	for i := drawchunk.start; i < drawchunk.end && i < len(edit.Buffer); i++ {
   257  		if i >= index {
   258  			break
   259  		}
   260  		x += FontWidth(font, string(edit.Buffer[i:i+1]))
   261  	}
   262  	if index >= len(edit.Buffer) && len(edit.Buffer) > 0 && edit.Buffer[len(edit.Buffer)-1] == '\n' {
   263  		return image.Point{x, drawchunk.Y + drawchunk.H + drawchunk.H/2}
   264  	}
   265  	return image.Point{x, drawchunk.Y + drawchunk.H/2}
   266  }
   267  
   268  func (state *TextEditor) doubleClick(coord image.Point) bool {
   269  	abs := func(x int) int {
   270  		if x < 0 {
   271  			return -x
   272  		}
   273  		return x
   274  	}
   275  	r := time.Since(state.lastClickTime) < 200*time.Millisecond && abs(state.lastClickCoord.X-coord.X) < 5 && abs(state.lastClickCoord.Y-coord.Y) < 5
   276  	state.lastClickCoord = coord
   277  	state.lastClickTime = time.Now()
   278  	return r
   279  
   280  }
   281  
   282  func (state *TextEditor) click(coord image.Point, font font.Face, row_height int) {
   283  	/* API click: on mouse down, move the cursor to the clicked location,
   284  	 * and reset the selection */
   285  	state.Cursor = state.locateCoord(coord, font, row_height)
   286  
   287  	state.SelectStart = state.Cursor
   288  	state.trueSelectStart = state.Cursor
   289  	state.SelectEnd = state.Cursor
   290  	state.HasPreferredX = false
   291  
   292  	switch state.clickCount {
   293  	case 2:
   294  		state.selectWord(state.SelectEnd)
   295  	case 3:
   296  		state.selectLine(state.SelectEnd)
   297  	}
   298  }
   299  
   300  func (state *TextEditor) drag(coord image.Point, font font.Face, row_height int) {
   301  	/* API drag: on mouse drag, move the cursor and selection endpoint
   302  	 * to the clicked location */
   303  	var p int = state.locateCoord(coord, font, row_height)
   304  	if state.SelectStart == state.SelectEnd {
   305  		state.SelectStart = state.Cursor
   306  		state.trueSelectStart = p
   307  	}
   308  	state.SelectEnd = p
   309  	state.Cursor = state.SelectEnd
   310  
   311  	switch state.clickCount {
   312  	case 2:
   313  		state.selectWord(p)
   314  	case 3:
   315  		state.selectLine(p)
   316  	}
   317  }
   318  
   319  func (state *TextEditor) selectWord(end int) {
   320  	state.SelectStart = state.trueSelectStart
   321  	state.SelectEnd = end
   322  	state.sortselection()
   323  	state.SelectStart = state.towd(state.SelectStart, -1, false)
   324  	state.SelectEnd = state.towd(state.SelectEnd, +1, true)
   325  }
   326  
   327  func (state *TextEditor) selectLine(end int) {
   328  	state.SelectStart = state.trueSelectStart
   329  	state.SelectEnd = end
   330  	state.sortselection()
   331  	state.SelectStart = state.tonl(state.SelectStart-1, -1)
   332  	state.SelectEnd = state.tonl(state.SelectEnd, +1)
   333  }
   334  
   335  func (state *TextEditor) clamp() {
   336  	/* make the selection/cursor state valid if client altered the string */
   337  	if state.hasSelection() {
   338  		if state.SelectStart > len(state.Buffer) {
   339  			state.SelectStart = len(state.Buffer)
   340  		}
   341  		if state.SelectEnd > len(state.Buffer) {
   342  			state.SelectEnd = len(state.Buffer)
   343  		}
   344  
   345  		/* if clamping forced them to be equal, move the cursor to match */
   346  		if state.SelectStart == state.SelectEnd {
   347  			state.Cursor = state.SelectStart
   348  		}
   349  	}
   350  
   351  	if state.Cursor > len(state.Buffer) {
   352  		state.Cursor = len(state.Buffer)
   353  	}
   354  }
   355  
   356  // Deletes a chunk of text in the editor.
   357  func (edit *TextEditor) Delete(where int, len int) {
   358  	/* delete characters while updating undo */
   359  	edit.makeundoDelete(where, len)
   360  
   361  	edit.Buffer = strDeleteText(edit.Buffer, where, len)
   362  	edit.HasPreferredX = false
   363  }
   364  
   365  // Deletes selection.
   366  func (edit *TextEditor) DeleteSelection() {
   367  	/* delete the section */
   368  	edit.clamp()
   369  
   370  	if edit.hasSelection() {
   371  		if edit.SelectStart < edit.SelectEnd {
   372  			edit.Delete(edit.SelectStart, edit.SelectEnd-edit.SelectStart)
   373  			edit.Cursor = edit.SelectStart
   374  			edit.SelectEnd = edit.Cursor
   375  		} else {
   376  			edit.Delete(edit.SelectEnd, edit.SelectStart-edit.SelectEnd)
   377  			edit.Cursor = edit.SelectEnd
   378  			edit.SelectStart = edit.Cursor
   379  		}
   380  
   381  		edit.HasPreferredX = false
   382  	}
   383  }
   384  
   385  func (state *TextEditor) sortselection() {
   386  	/* canonicalize the selection so start <= end */
   387  	if state.SelectEnd < state.SelectStart {
   388  		var temp int = state.SelectEnd
   389  		state.SelectEnd = state.SelectStart
   390  		state.SelectStart = temp
   391  	}
   392  }
   393  
   394  func (state *TextEditor) moveToFirst() {
   395  	/* move cursor to first character of selection */
   396  	if state.hasSelection() {
   397  		state.sortselection()
   398  		state.Cursor = state.SelectStart
   399  		state.SelectEnd = state.SelectStart
   400  		state.HasPreferredX = false
   401  	}
   402  }
   403  
   404  func (state *TextEditor) moveToLast() {
   405  	/* move cursor to last character of selection */
   406  	if state.hasSelection() {
   407  		state.sortselection()
   408  		state.clamp()
   409  		state.Cursor = state.SelectEnd
   410  		state.SelectStart = state.SelectEnd
   411  		state.HasPreferredX = false
   412  	}
   413  }
   414  
   415  // Moves to the beginning or end of a line
   416  func (state *TextEditor) tonl(start int, dir int) int {
   417  	sz := len(state.Buffer)
   418  
   419  	i := start
   420  	if i < 0 {
   421  		return 0
   422  	}
   423  	if i >= sz {
   424  		i = sz - 1
   425  	}
   426  	for ; (i >= 0) && (i < sz); i += dir {
   427  		c := state.Buffer[i]
   428  
   429  		if c == '\n' {
   430  			if dir >= 0 {
   431  				return i
   432  			} else {
   433  				return i + 1
   434  			}
   435  		}
   436  	}
   437  	if dir < 0 {
   438  		return 0
   439  	} else {
   440  		return sz
   441  	}
   442  }
   443  
   444  // Moves to the beginning or end of an alphanumerically delimited word
   445  func (state *TextEditor) towd(start int, dir int, dontForceAdvance bool) int {
   446  	first := (dir < 0)
   447  	notfirst := !first
   448  	var i int
   449  	for i = start; (i >= 0) && (i < len(state.Buffer)); i += dir {
   450  		c := state.Buffer[i]
   451  		if !(unicode.IsLetter(c) || unicode.IsDigit(c) || (c == '_')) {
   452  			if !first && !dontForceAdvance {
   453  				i++
   454  			}
   455  			break
   456  		}
   457  		first = notfirst
   458  	}
   459  	if i < 0 {
   460  		i = 0
   461  	}
   462  	return i
   463  }
   464  
   465  func (state *TextEditor) prepSelectionAtCursor() {
   466  	/* update selection and cursor to match each other */
   467  	if !state.hasSelection() {
   468  		state.SelectEnd = state.Cursor
   469  		state.SelectStart = state.SelectEnd
   470  	} else {
   471  		state.Cursor = state.SelectEnd
   472  	}
   473  }
   474  
   475  func (edit *TextEditor) Cut() int {
   476  	if edit.Flags&EditReadOnly != 0 {
   477  		return 0
   478  	}
   479  	/* API cut: delete selection */
   480  	if edit.hasSelection() {
   481  		edit.DeleteSelection() /* implicitly clamps */
   482  		edit.HasPreferredX = false
   483  		return 1
   484  	}
   485  
   486  	return 0
   487  }
   488  
   489  // Paste from clipboard
   490  func (edit *TextEditor) Paste(ctext string) {
   491  	if edit.Flags&EditReadOnly != 0 {
   492  		return
   493  	}
   494  
   495  	/* if there's a selection, the paste should delete it */
   496  	edit.clamp()
   497  
   498  	edit.DeleteSelection()
   499  
   500  	text := []rune(ctext)
   501  
   502  	edit.Buffer = strInsertText(edit.Buffer, edit.Cursor, text)
   503  
   504  	edit.makeundoInsert(edit.Cursor, len(text))
   505  	edit.Cursor += len(text)
   506  	edit.HasPreferredX = false
   507  }
   508  
   509  func (edit *TextEditor) Text(text []rune) {
   510  	if edit.Flags&EditReadOnly != 0 {
   511  		return
   512  	}
   513  
   514  	for i := range text {
   515  		/* can't add newline in single-line mode */
   516  		if text[i] == '\n' && edit.SingleLine {
   517  			break
   518  		}
   519  
   520  		/* can't add tab in single-line mode */
   521  		if text[i] == '\t' && edit.SingleLine {
   522  			break
   523  		}
   524  
   525  		/* filter incoming text */
   526  		if edit.Filter != nil && !edit.Filter(text[i]) {
   527  			continue
   528  		}
   529  
   530  		if edit.InsertMode && !edit.hasSelection() && edit.Cursor < len(edit.Buffer) {
   531  			edit.makeundoReplace(edit.Cursor, 1, 1)
   532  			edit.Buffer = strDeleteText(edit.Buffer, edit.Cursor, 1)
   533  			edit.Buffer = strInsertText(edit.Buffer, edit.Cursor, text[i:i+1])
   534  			edit.Cursor++
   535  			edit.HasPreferredX = false
   536  		} else {
   537  			edit.DeleteSelection() /* implicitly clamps */
   538  			edit.Buffer = strInsertText(edit.Buffer, edit.Cursor, text[i:i+1])
   539  			edit.makeundoInsert(edit.Cursor, 1)
   540  			edit.Cursor++
   541  			edit.HasPreferredX = false
   542  		}
   543  	}
   544  }
   545  
   546  func (state *TextEditor) key(e gkey.Event, font font.Face, row_height int, area_height int) {
   547  	readOnly := state.Flags&EditReadOnly != 0
   548  retry:
   549  	switch e.Code {
   550  	case gkey.CodeZ:
   551  		if readOnly {
   552  			return
   553  		}
   554  		if e.Modifiers&gkey.ModCtrl != 0 {
   555  			if e.Modifiers&gkey.ModShift != 0 {
   556  				state.DoRedo()
   557  				state.HasPreferredX = false
   558  
   559  			} else {
   560  				state.DoUndo()
   561  				state.HasPreferredX = false
   562  			}
   563  		}
   564  
   565  	case gkey.CodeK:
   566  		if readOnly {
   567  			return
   568  		}
   569  		if e.Modifiers&gkey.ModCtrl != 0 {
   570  			state.trueSelectStart = state.Cursor
   571  			state.selectLine(state.Cursor)
   572  			state.DeleteSelection()
   573  		}
   574  
   575  	case gkey.CodeInsert:
   576  		state.InsertMode = !state.InsertMode
   577  
   578  	case gkey.CodeLeftArrow:
   579  		if e.Modifiers&gkey.ModCtrl != 0 {
   580  			if e.Modifiers&gkey.ModShift != 0 {
   581  				if !state.hasSelection() {
   582  					state.prepSelectionAtCursor()
   583  				}
   584  				state.Cursor = state.towd(state.Cursor-1, -1, false)
   585  				state.SelectEnd = state.Cursor
   586  				state.clamp()
   587  			} else {
   588  				if state.hasSelection() {
   589  					state.moveToFirst()
   590  				} else {
   591  					state.Cursor = state.towd(state.Cursor-1, -1, false)
   592  					state.clamp()
   593  				}
   594  			}
   595  		} else {
   596  			if e.Modifiers&gkey.ModShift != 0 {
   597  				state.clamp()
   598  				state.prepSelectionAtCursor()
   599  
   600  				/* move selection left */
   601  				if state.SelectEnd > 0 {
   602  					state.SelectEnd--
   603  				}
   604  				state.Cursor = state.SelectEnd
   605  				state.HasPreferredX = false
   606  			} else {
   607  				/* if currently there's a selection,
   608  				 * move cursor to start of selection */
   609  				if state.hasSelection() {
   610  					state.moveToFirst()
   611  				} else if state.Cursor > 0 {
   612  					state.Cursor--
   613  				}
   614  				state.HasPreferredX = false
   615  			}
   616  		}
   617  
   618  	case gkey.CodeRightArrow:
   619  		if e.Modifiers&gkey.ModCtrl != 0 {
   620  			if e.Modifiers&gkey.ModShift != 0 {
   621  				if !state.hasSelection() {
   622  					state.prepSelectionAtCursor()
   623  				}
   624  				state.Cursor = state.towd(state.Cursor, +1, false)
   625  				state.SelectEnd = state.Cursor
   626  				state.clamp()
   627  			} else {
   628  				if state.hasSelection() {
   629  					state.moveToLast()
   630  				} else {
   631  					state.Cursor = state.towd(state.Cursor, +1, false)
   632  					state.clamp()
   633  				}
   634  			}
   635  		} else {
   636  			if e.Modifiers&gkey.ModShift != 0 {
   637  				state.prepSelectionAtCursor()
   638  
   639  				/* move selection right */
   640  				state.SelectEnd++
   641  
   642  				state.clamp()
   643  				state.Cursor = state.SelectEnd
   644  				state.HasPreferredX = false
   645  			} else {
   646  				/* if currently there's a selection,
   647  				 * move cursor to end of selection */
   648  				if state.hasSelection() {
   649  					state.moveToLast()
   650  				} else {
   651  					state.Cursor++
   652  				}
   653  				state.clamp()
   654  				state.HasPreferredX = false
   655  			}
   656  		}
   657  	case gkey.CodeDownArrow:
   658  		if state.SingleLine {
   659  			e.Code = gkey.CodeRightArrow
   660  			goto retry
   661  		}
   662  		state.verticalCursorMove(e, font, row_height, +row_height)
   663  
   664  	case gkey.CodeUpArrow:
   665  		if state.SingleLine {
   666  			e.Code = gkey.CodeRightArrow
   667  			goto retry
   668  		}
   669  		state.verticalCursorMove(e, font, row_height, -row_height)
   670  
   671  	case gkey.CodePageDown:
   672  		if state.SingleLine {
   673  			break
   674  		}
   675  		state.verticalCursorMove(e, font, row_height, +area_height/2)
   676  
   677  	case gkey.CodePageUp:
   678  		if state.SingleLine {
   679  			break
   680  		}
   681  		state.verticalCursorMove(e, font, row_height, -area_height/2)
   682  
   683  	case gkey.CodeDeleteForward:
   684  		if readOnly {
   685  			return
   686  		}
   687  		if state.hasSelection() {
   688  			state.DeleteSelection()
   689  		} else {
   690  			if state.Cursor < len(state.Buffer) {
   691  				state.Delete(state.Cursor, 1)
   692  			}
   693  		}
   694  
   695  		state.HasPreferredX = false
   696  
   697  	case gkey.CodeDeleteBackspace:
   698  		if readOnly {
   699  			return
   700  		}
   701  		switch {
   702  		case state.hasSelection():
   703  			state.DeleteSelection()
   704  		case e.Modifiers&gkey.ModCtrl != 0:
   705  			state.SelectEnd = state.Cursor
   706  			state.SelectStart = state.towd(state.Cursor-1, -1, false)
   707  			state.DeleteSelection()
   708  		default:
   709  			state.clamp()
   710  			if state.Cursor > 0 {
   711  				state.Delete(state.Cursor-1, 1)
   712  				state.Cursor--
   713  			}
   714  		}
   715  		state.HasPreferredX = false
   716  
   717  	case gkey.CodeHome:
   718  		if e.Modifiers&gkey.ModCtrl != 0 {
   719  			if e.Modifiers&gkey.ModShift != 0 {
   720  				state.prepSelectionAtCursor()
   721  				state.SelectEnd = 0
   722  				state.Cursor = state.SelectEnd
   723  				state.HasPreferredX = false
   724  			} else {
   725  				state.SelectEnd = 0
   726  				state.SelectStart = state.SelectEnd
   727  				state.Cursor = state.SelectStart
   728  				state.HasPreferredX = false
   729  			}
   730  		} else {
   731  			state.clamp()
   732  			start := state.tonl(state.Cursor-1, -1)
   733  			if e.Modifiers&gkey.ModShift != 0 {
   734  				state.clamp()
   735  				state.prepSelectionAtCursor()
   736  				state.SelectEnd = start
   737  				state.Cursor = state.SelectEnd
   738  				state.HasPreferredX = false
   739  			} else {
   740  				state.clamp()
   741  				state.moveToFirst()
   742  				state.Cursor = start
   743  				state.HasPreferredX = false
   744  			}
   745  		}
   746  
   747  	case gkey.CodeA:
   748  		if e.Modifiers&gkey.ModCtrl != 0 {
   749  			state.clamp()
   750  			state.moveToFirst()
   751  			state.Cursor = state.tonl(state.Cursor-1, -1)
   752  			state.HasPreferredX = false
   753  		}
   754  
   755  	case gkey.CodeEnd:
   756  		if e.Modifiers&gkey.ModCtrl != 0 {
   757  			if e.Modifiers&gkey.ModShift != 0 {
   758  				state.prepSelectionAtCursor()
   759  				state.SelectEnd = len(state.Buffer)
   760  				state.Cursor = state.SelectEnd
   761  				state.HasPreferredX = false
   762  			} else {
   763  				state.Cursor = len(state.Buffer)
   764  				state.SelectEnd = 0
   765  				state.SelectStart = state.SelectEnd
   766  				state.HasPreferredX = false
   767  			}
   768  		} else {
   769  			state.clamp()
   770  			end := state.tonl(state.Cursor, +1)
   771  			if e.Modifiers&gkey.ModShift != 0 {
   772  				state.clamp()
   773  				state.prepSelectionAtCursor()
   774  				state.HasPreferredX = false
   775  				state.Cursor = end
   776  				state.SelectEnd = state.Cursor
   777  			} else {
   778  				state.clamp()
   779  				state.moveToFirst()
   780  				state.HasPreferredX = false
   781  				state.Cursor = end
   782  			}
   783  		}
   784  
   785  	case gkey.CodeE:
   786  		if e.Modifiers&gkey.ModCtrl != 0 {
   787  			end := state.tonl(state.Cursor, +1)
   788  			state.clamp()
   789  			state.moveToFirst()
   790  			state.HasPreferredX = false
   791  			state.Cursor = end
   792  		}
   793  	}
   794  }
   795  
   796  func (state *TextEditor) verticalCursorMove(e gkey.Event, font font.Face, row_height int, offset int) {
   797  	if e.Modifiers&gkey.ModShift != 0 {
   798  		state.prepSelectionAtCursor()
   799  	} else if state.hasSelection() {
   800  		if offset < 0 {
   801  			state.moveToFirst()
   802  		} else {
   803  			state.moveToLast()
   804  		}
   805  	}
   806  
   807  	state.clamp()
   808  
   809  	p := state.indexToCoord(state.Cursor, font, row_height)
   810  	p.Y += offset
   811  
   812  	if state.HasPreferredX {
   813  		p.X = state.PreferredX
   814  	} else {
   815  		state.HasPreferredX = true
   816  		state.PreferredX = p.X
   817  	}
   818  	state.Cursor = state.locateCoord(p, font, row_height)
   819  
   820  	state.clamp()
   821  
   822  	if e.Modifiers&gkey.ModShift != 0 {
   823  		state.SelectEnd = state.Cursor
   824  	}
   825  }
   826  
   827  func texteditFlushRedo(state *textUndoState) {
   828  	state.RedoPoint = int16(_TEXTEDIT_UNDOSTATECOUNT)
   829  }
   830  
   831  func texteditDiscardUndo(state *textUndoState) {
   832  	/* discard the oldest entry in the undo list */
   833  	if state.UndoPoint > 0 {
   834  		state.UndoPoint--
   835  		copy(state.UndoRec[:], state.UndoRec[1:])
   836  	}
   837  }
   838  
   839  func texteditCreateUndoRecord(state *textUndoState, numchars int) *textUndoRecord {
   840  	/* any time we create a new undo record, we discard redo*/
   841  	texteditFlushRedo(state)
   842  
   843  	/* if we have no free records, we have to make room,
   844  	 * by sliding the existing records down */
   845  	if int(state.UndoPoint) == _TEXTEDIT_UNDOSTATECOUNT {
   846  		texteditDiscardUndo(state)
   847  	}
   848  
   849  	r := &state.UndoRec[state.UndoPoint]
   850  	state.UndoPoint++
   851  	return r
   852  }
   853  
   854  func texteditCreateundo(state *textUndoState, pos int, insert_len int, delete_len int) *textUndoRecord {
   855  	r := texteditCreateUndoRecord(state, insert_len)
   856  
   857  	r.Where = pos
   858  	r.InsertLength = insert_len
   859  	r.DeleteLength = delete_len
   860  	r.Text = nil
   861  
   862  	return r
   863  }
   864  
   865  func (edit *TextEditor) DoUndo() {
   866  	var s *textUndoState = &edit.Undo
   867  	var u textUndoRecord
   868  	var r *textUndoRecord
   869  	if s.UndoPoint == 0 {
   870  		return
   871  	}
   872  
   873  	/* we need to do two things: apply the undo record, and create a redo record */
   874  	u = s.UndoRec[s.UndoPoint-1]
   875  
   876  	r = &s.UndoRec[s.RedoPoint-1]
   877  	r.Text = nil
   878  
   879  	r.InsertLength = u.DeleteLength
   880  	r.DeleteLength = u.InsertLength
   881  	r.Where = u.Where
   882  
   883  	if u.DeleteLength != 0 {
   884  		r.Text = make([]rune, u.DeleteLength)
   885  		copy(r.Text, edit.Buffer[u.Where:u.Where+u.DeleteLength])
   886  		edit.Buffer = strDeleteText(edit.Buffer, u.Where, u.DeleteLength)
   887  	}
   888  
   889  	/* check type of recorded action: */
   890  	if u.InsertLength != 0 {
   891  		/* easy case: was a deletion, so we need to insert n characters */
   892  		edit.Buffer = strInsertText(edit.Buffer, u.Where, u.Text)
   893  	}
   894  
   895  	edit.Cursor = u.Where + u.InsertLength
   896  
   897  	s.UndoPoint--
   898  	s.RedoPoint--
   899  }
   900  
   901  func (edit *TextEditor) DoRedo() {
   902  	var s *textUndoState = &edit.Undo
   903  	var u *textUndoRecord
   904  	var r textUndoRecord
   905  	if int(s.RedoPoint) == _TEXTEDIT_UNDOSTATECOUNT {
   906  		return
   907  	}
   908  
   909  	/* we need to do two things: apply the redo record, and create an undo record */
   910  	u = &s.UndoRec[s.UndoPoint]
   911  
   912  	r = s.UndoRec[s.RedoPoint]
   913  
   914  	/* we KNOW there must be room for the undo record, because the redo record
   915  	was derived from an undo record */
   916  	u.DeleteLength = r.InsertLength
   917  
   918  	u.InsertLength = r.DeleteLength
   919  	u.Where = r.Where
   920  	u.Text = nil
   921  
   922  	if r.DeleteLength != 0 {
   923  		u.Text = make([]rune, r.DeleteLength)
   924  		copy(u.Text, edit.Buffer[r.Where:r.Where+r.DeleteLength])
   925  		edit.Buffer = strDeleteText(edit.Buffer, r.Where, r.DeleteLength)
   926  	}
   927  
   928  	if r.InsertLength != 0 {
   929  		/* easy case: need to insert n characters */
   930  		edit.Buffer = strInsertText(edit.Buffer, r.Where, r.Text)
   931  	}
   932  
   933  	edit.Cursor = r.Where + r.InsertLength
   934  
   935  	s.UndoPoint++
   936  	s.RedoPoint++
   937  }
   938  
   939  func (state *TextEditor) makeundoInsert(where int, length int) {
   940  	texteditCreateundo(&state.Undo, where, 0, length)
   941  }
   942  
   943  func (state *TextEditor) makeundoDelete(where int, length int) {
   944  	u := texteditCreateundo(&state.Undo, where, length, 0)
   945  	u.Text = make([]rune, length)
   946  	copy(u.Text, state.Buffer[where:where+length])
   947  }
   948  
   949  func (state *TextEditor) makeundoReplace(where int, old_length int, new_length int) {
   950  	u := texteditCreateundo(&state.Undo, where, old_length, new_length)
   951  	u.Text = make([]rune, old_length)
   952  	copy(u.Text, state.Buffer[where:where+old_length])
   953  }
   954  
   955  func (state *TextEditor) clearState(type_ TextEditType) {
   956  	/* reset the state to default */
   957  	state.Undo.UndoPoint = 0
   958  
   959  	state.Undo.RedoPoint = int16(_TEXTEDIT_UNDOSTATECOUNT)
   960  	state.HasPreferredX = false
   961  	state.PreferredX = 0
   962  	//state.CursorAtEndOfLine = 0
   963  	state.Initialized = true
   964  	state.SingleLine = type_ == TextEditSingleLine
   965  	state.InsertMode = false
   966  }
   967  
   968  func (edit *TextEditor) SelectAll() {
   969  	edit.SelectStart = 0
   970  	edit.SelectEnd = len(edit.Buffer)
   971  }
   972  
   973  func (edit *TextEditor) editDrawText(out *command.Buffer, style *nstyle.Edit, pos image.Point, x_margin int, text []rune, textOffset int, row_height int, f font.Face, background color.RGBA, foreground color.RGBA, is_selected bool) (posOut image.Point) {
   974  	if len(text) == 0 {
   975  		return pos
   976  	}
   977  	var line_offset int = 0
   978  	var line_count int = 0
   979  	var txt textWidget
   980  	txt.Background = background
   981  	txt.Text = foreground
   982  
   983  	pos_x, pos_y := pos.X, pos.Y
   984  	start := 0
   985  
   986  	tabsz := glyphAdvance(f, ' ') * tabSizeInSpaces
   987  	pwsz := glyphAdvance(f, '*')
   988  
   989  	measureText := func(start, end int) int {
   990  		if edit.PasswordChar != 0 {
   991  			return pwsz * (end - start)
   992  		}
   993  		// XXX calculating text width here is slow figure out why
   994  		return measureRunes(f, text[start:end])
   995  	}
   996  
   997  	getText := func(start, end int) string {
   998  		if edit.PasswordChar != 0 {
   999  			n := end - start
  1000  			if n >= len(edit.password) {
  1001  				edit.password = make([]rune, n)
  1002  				for i := range edit.password {
  1003  					edit.password[i] = edit.PasswordChar
  1004  				}
  1005  			}
  1006  			return string(edit.password[:n])
  1007  		}
  1008  		return string(text[start:end])
  1009  	}
  1010  
  1011  	flushLine := func(index int) rect.Rect {
  1012  		// new line sepeator so draw previous line
  1013  		var lblrect rect.Rect
  1014  		lblrect.Y = pos_y + line_offset
  1015  		lblrect.H = row_height
  1016  		lblrect.W = nk_null_rect.W
  1017  		lblrect.X = pos_x
  1018  
  1019  		if is_selected { // selection needs to draw different background color
  1020  			if index == len(text) || (index == start && start == 0) {
  1021  				lblrect.W = measureText(start, index)
  1022  			}
  1023  			out.FillRect(lblrect, 0, background)
  1024  		}
  1025  		edit.drawchunks = append(edit.drawchunks, drawchunk{lblrect, start + textOffset, index + textOffset})
  1026  		widgetText(out, lblrect, getText(start, index), &txt, "LC", f)
  1027  
  1028  		pos_x = x_margin
  1029  
  1030  		return lblrect
  1031  	}
  1032  
  1033  	flushTab := func(index int) rect.Rect {
  1034  		var lblrect rect.Rect
  1035  		lblrect.Y = pos_y + line_offset
  1036  		lblrect.H = row_height
  1037  		lblrect.W = measureText(start, index)
  1038  		lblrect.X = pos_x
  1039  
  1040  		lblrect.W = int(math.Floor(float64(lblrect.X+lblrect.W-x_margin)/float64(tabsz))+1)*tabsz + x_margin - lblrect.X
  1041  
  1042  		if is_selected {
  1043  			out.FillRect(lblrect, 0, background)
  1044  		}
  1045  		edit.drawchunks = append(edit.drawchunks, drawchunk{lblrect, start + textOffset, index + textOffset})
  1046  		widgetText(out, lblrect, getText(start, index), &txt, "LC", f)
  1047  
  1048  		pos_x += lblrect.W
  1049  
  1050  		return lblrect
  1051  	}
  1052  
  1053  	for index, glyph := range text {
  1054  		switch glyph {
  1055  		case '\t':
  1056  			flushTab(index)
  1057  			start = index + 1
  1058  		case '\n':
  1059  			flushLine(index)
  1060  			line_count++
  1061  			start = index + 1
  1062  			line_offset += row_height
  1063  
  1064  		case '\r':
  1065  			// do nothing
  1066  		}
  1067  	}
  1068  
  1069  	if start >= len(text) {
  1070  		return image.Point{pos_x, pos_y + line_offset}
  1071  	}
  1072  
  1073  	// draw last line
  1074  	lblrect := flushLine(len(text))
  1075  	lblrect.W = measureText(start, len(text))
  1076  
  1077  	return image.Point{lblrect.X + lblrect.W, lblrect.Y}
  1078  }
  1079  
  1080  func (ed *TextEditor) doEdit(bounds rect.Rect, style *nstyle.Edit, inp *Input, cut, copy, paste bool) (ret EditEvents) {
  1081  	font := ed.win.ctx.Style.Font
  1082  	state := ed.win.widgets.PrevState(bounds)
  1083  
  1084  	ed.clamp()
  1085  
  1086  	// visible text area calculation
  1087  	var area rect.Rect
  1088  	area.X = bounds.X + style.Padding.X + style.Border
  1089  	area.Y = bounds.Y + style.Padding.Y + style.Border
  1090  	area.W = bounds.W - (2.0*style.Padding.X + 2*style.Border)
  1091  	area.H = bounds.H - (2.0*style.Padding.Y + 2*style.Border)
  1092  	if ed.Flags&EditMultiline != 0 {
  1093  		area.H = area.H - style.ScrollbarSize.Y
  1094  	}
  1095  	var row_height int
  1096  	if ed.Flags&EditMultiline != 0 {
  1097  		row_height = FontHeight(font) + style.RowPadding
  1098  	} else {
  1099  		row_height = area.H
  1100  	}
  1101  
  1102  	/* update edit state */
  1103  	prev_state := ed.Active
  1104  
  1105  	if ed.win.ctx.activateEditor != nil {
  1106  		if ed.win.ctx.activateEditor == ed {
  1107  			ed.Active = true
  1108  			if ed.win.flags&windowDocked != 0 {
  1109  				ed.win.ctx.dockedWindowFocus = ed.win.idx
  1110  			}
  1111  		} else {
  1112  			ed.Active = false
  1113  		}
  1114  	}
  1115  
  1116  	is_hovered := inp.Mouse.HoveringRect(bounds)
  1117  
  1118  	if ed.Flags&EditFocusFollowsMouse != 0 {
  1119  		if inp != nil {
  1120  			ed.Active = is_hovered
  1121  		}
  1122  	} else {
  1123  		if inp != nil && inp.Mouse.Buttons[pointer.ButtonLeft].Clicked && inp.Mouse.Buttons[pointer.ButtonLeft].Down {
  1124  			ed.Active = inp.Mouse.HoveringRect(bounds)
  1125  		}
  1126  	}
  1127  
  1128  	/* (de)activate text editor */
  1129  	var select_all bool
  1130  	if !prev_state && ed.Active {
  1131  		type_ := TextEditSingleLine
  1132  		if ed.Flags&EditMultiline != 0 {
  1133  			type_ = TextEditMultiLine
  1134  		}
  1135  		ed.clearState(type_)
  1136  		if ed.Flags&EditAlwaysInsertMode != 0 {
  1137  			ed.InsertMode = true
  1138  		}
  1139  		if ed.Flags&EditAutoSelect != 0 {
  1140  			select_all = true
  1141  		}
  1142  	} else if !ed.Active {
  1143  		ed.InsertMode = false
  1144  	}
  1145  
  1146  	if ed.Flags&EditNeverInsertMode != 0 {
  1147  		ed.InsertMode = false
  1148  	}
  1149  
  1150  	if ed.Active {
  1151  		ret = EditActive
  1152  	} else {
  1153  		ret = EditInactive
  1154  	}
  1155  	if prev_state != ed.Active {
  1156  		if ed.Active {
  1157  			ret |= EditActivated
  1158  		} else {
  1159  			ret |= EditDeactivated
  1160  		}
  1161  	}
  1162  
  1163  	/* handle user input */
  1164  	cursor_follow := ed.CursorFollow
  1165  	ed.CursorFollow = false
  1166  	if ed.Active && inp != nil {
  1167  		inpos := inp.Mouse.Pos
  1168  		indelta := inp.Mouse.Delta
  1169  		coord := image.Point{(inpos.X - area.X), (inpos.Y - area.Y)}
  1170  
  1171  		var isHovered bool
  1172  		{
  1173  			areaWithoutScrollbar := area
  1174  			areaWithoutScrollbar.W -= style.ScrollbarSize.X
  1175  			isHovered = inp.Mouse.HoveringRect(areaWithoutScrollbar)
  1176  		}
  1177  
  1178  		var autoscrollTop bool
  1179  		{
  1180  			a := area
  1181  			a.W -= style.ScrollbarSize.X
  1182  			a.H = FontHeight(font) / 2
  1183  			autoscrollTop = inp.Mouse.HoveringRect(a) && inp.Mouse.Buttons[pointer.ButtonLeft].Down
  1184  		}
  1185  
  1186  		var autoscrollBot bool
  1187  		{
  1188  			a := area
  1189  			a.W -= style.ScrollbarSize.X
  1190  			a.Y = a.Y + a.H - FontHeight(font)/2
  1191  			a.H = FontHeight(font) / 2
  1192  			autoscrollBot = inp.Mouse.HoveringRect(a) && inp.Mouse.Buttons[pointer.ButtonLeft].Down
  1193  		}
  1194  
  1195  		/* mouse click handler */
  1196  		if select_all {
  1197  			ed.SelectAll()
  1198  		} else if isHovered && inp.Mouse.Buttons[pointer.ButtonLeft].Down && inp.Mouse.Buttons[pointer.ButtonLeft].Clicked {
  1199  			if ed.doubleClick(coord) {
  1200  				ed.clickCount++
  1201  				if ed.clickCount > 3 {
  1202  					ed.clickCount = 3
  1203  				}
  1204  			} else {
  1205  				ed.clickCount = 1
  1206  			}
  1207  			ed.click(coord, font, row_height)
  1208  		} else if isHovered && inp.Mouse.Buttons[pointer.ButtonLeft].Down && (indelta.X != 0.0 || indelta.Y != 0.0) {
  1209  			ed.drag(coord, font, row_height)
  1210  			cursor_follow = true
  1211  		} else if autoscrollTop {
  1212  			coord1 := coord
  1213  			coord1.Y -= FontHeight(font)
  1214  			ed.drag(coord1, font, row_height)
  1215  			cursor_follow = true
  1216  		} else if autoscrollBot {
  1217  			coord1 := coord
  1218  			coord1.Y += FontHeight(font)
  1219  			ed.drag(coord1, font, row_height)
  1220  			cursor_follow = true
  1221  		}
  1222  
  1223  		/* text input */
  1224  		if inp.Keyboard.Text != "" {
  1225  			ed.Text([]rune(inp.Keyboard.Text))
  1226  			cursor_follow = true
  1227  		}
  1228  
  1229  		clipboardModifier := gkey.ModCtrl
  1230  		if runtime.GOOS == "darwin" {
  1231  			clipboardModifier = gkey.ModMeta
  1232  		}
  1233  
  1234  		for _, e := range inp.Keyboard.Keys {
  1235  			switch e.Code {
  1236  			case gkey.CodeReturnEnter:
  1237  				if ed.Flags&EditCtrlEnterNewline != 0 && e.Modifiers&gkey.ModShift != 0 {
  1238  					ed.Text([]rune{'\n'})
  1239  					cursor_follow = true
  1240  				} else if ed.Flags&EditSigEnter != 0 {
  1241  					ret = EditInactive
  1242  					ret |= EditDeactivated
  1243  					if ed.Flags&EditReadOnly == 0 {
  1244  						ret |= EditCommitted
  1245  					}
  1246  					ed.Active = false
  1247  				}
  1248  
  1249  			case gkey.CodeX:
  1250  				if e.Modifiers&clipboardModifier != 0 {
  1251  					cut = true
  1252  				}
  1253  
  1254  			case gkey.CodeC:
  1255  				if e.Modifiers&clipboardModifier != 0 {
  1256  					copy = true
  1257  				}
  1258  
  1259  			case gkey.CodeV:
  1260  				if e.Modifiers&clipboardModifier != 0 {
  1261  					paste = true
  1262  				}
  1263  
  1264  			case gkey.CodeF:
  1265  				if e.Modifiers&clipboardModifier != 0 {
  1266  					ed.popupFind()
  1267  				}
  1268  
  1269  			case gkey.CodeG:
  1270  				if e.Modifiers&clipboardModifier != 0 {
  1271  					ed.lookForward(true)
  1272  					cursor_follow = true
  1273  				}
  1274  
  1275  			default:
  1276  				ed.key(e, font, row_height, area.H)
  1277  				cursor_follow = true
  1278  			}
  1279  
  1280  		}
  1281  
  1282  		/* cut & copy handler */
  1283  		if (copy || cut) && (ed.Flags&EditClipboard != 0) {
  1284  			var begin, end int
  1285  			if ed.SelectStart > ed.SelectEnd {
  1286  				begin = ed.SelectEnd
  1287  				end = ed.SelectStart
  1288  			} else {
  1289  				begin = ed.SelectStart
  1290  				end = ed.SelectEnd
  1291  			}
  1292  			clipboard.Set(string(ed.Buffer[begin:end]))
  1293  			if cut {
  1294  				ed.Cut()
  1295  				cursor_follow = true
  1296  			}
  1297  		}
  1298  
  1299  		/* paste handler */
  1300  		if paste && (ed.Flags&EditClipboard != 0) {
  1301  			ed.Paste(clipboard.Get())
  1302  			cursor_follow = true
  1303  		}
  1304  
  1305  	}
  1306  
  1307  	/* set widget state */
  1308  	if ed.Active {
  1309  		state = nstyle.WidgetStateActive
  1310  	} else {
  1311  		state = nstyle.WidgetStateInactive
  1312  	}
  1313  	if is_hovered {
  1314  		state |= nstyle.WidgetStateHovered
  1315  	}
  1316  
  1317  	var d drawableTextEditor
  1318  
  1319  	/* text pointer positions */
  1320  	var selection_begin, selection_end int
  1321  	if ed.SelectStart < ed.SelectEnd {
  1322  		selection_begin = ed.SelectStart
  1323  		selection_end = ed.SelectEnd
  1324  	} else {
  1325  		selection_begin = ed.SelectEnd
  1326  		selection_end = ed.SelectStart
  1327  	}
  1328  
  1329  	d.SelectionBegin, d.SelectionEnd = selection_begin, selection_end
  1330  
  1331  	d.Edit = ed
  1332  	d.State = state
  1333  	d.Style = style
  1334  	d.Scaling = ed.win.ctx.Style.Scaling
  1335  	d.Bounds = bounds
  1336  	d.Area = area
  1337  	d.RowHeight = row_height
  1338  	d.hasInput = inp.Mouse.valid
  1339  	ed.win.widgets.Add(state, bounds)
  1340  	d.Draw(&ed.win.ctx.Style, &ed.win.cmds)
  1341  
  1342  	/* scrollbar */
  1343  	if cursor_follow {
  1344  		cursor_pos := d.CursorPos
  1345  		/* update scrollbar to follow cursor */
  1346  		if ed.Flags&EditNoHorizontalScroll == 0 {
  1347  			/* horizontal scroll */
  1348  			scroll_increment := area.W / 2
  1349  			if (cursor_pos.X < ed.Scrollbar.X) || ((ed.Scrollbar.X+area.W)-cursor_pos.X < FontWidth(font, "i")) {
  1350  				ed.Scrollbar.X = max(0, cursor_pos.X-scroll_increment)
  1351  			}
  1352  		} else {
  1353  			ed.Scrollbar.X = 0
  1354  		}
  1355  
  1356  		if ed.Flags&EditMultiline != 0 {
  1357  			/* vertical scroll */
  1358  			if cursor_pos.Y < ed.Scrollbar.Y {
  1359  				ed.Scrollbar.Y = max(0, cursor_pos.Y-row_height)
  1360  			}
  1361  			for (ed.Scrollbar.Y+area.H)-cursor_pos.Y < row_height {
  1362  				ed.Scrollbar.Y = ed.Scrollbar.Y + row_height
  1363  			}
  1364  		} else {
  1365  			ed.Scrollbar.Y = 0
  1366  		}
  1367  	}
  1368  
  1369  	if !ed.SingleLine {
  1370  		/* scrollbar widget */
  1371  		var scroll rect.Rect
  1372  		scroll.X = (area.X + area.W) - style.ScrollbarSize.X
  1373  		scroll.Y = area.Y
  1374  		scroll.W = style.ScrollbarSize.X
  1375  		scroll.H = area.H
  1376  
  1377  		scroll_offset := float64(ed.Scrollbar.Y)
  1378  		scroll_step := float64(scroll.H) * 0.1
  1379  		scroll_inc := float64(scroll.H) * 0.01
  1380  		scroll_target := float64(d.TextSize.Y + row_height)
  1381  		ed.Scrollbar.Y = int(doScrollbarv(ed.win, scroll, bounds, scroll_offset, scroll_target, scroll_step, scroll_inc, &style.Scrollbar, inp, font))
  1382  	}
  1383  
  1384  	return ret
  1385  }
  1386  
  1387  type drawableTextEditor struct {
  1388  	Edit      *TextEditor
  1389  	State     nstyle.WidgetStates
  1390  	Style     *nstyle.Edit
  1391  	Scaling   float64
  1392  	Bounds    rect.Rect
  1393  	Area      rect.Rect
  1394  	RowHeight int
  1395  	hasInput  bool
  1396  
  1397  	SelectionBegin, SelectionEnd int
  1398  
  1399  	TextSize  image.Point
  1400  	CursorPos image.Point
  1401  }
  1402  
  1403  func (d *drawableTextEditor) Draw(z *nstyle.Style, out *command.Buffer) {
  1404  	edit := d.Edit
  1405  	state := d.State
  1406  	style := d.Style
  1407  	bounds := d.Bounds
  1408  	font := z.Font
  1409  	area := d.Area
  1410  	row_height := d.RowHeight
  1411  	selection_begin := d.SelectionBegin
  1412  	selection_end := d.SelectionEnd
  1413  	if edit.drawchunks != nil {
  1414  		edit.drawchunks = edit.drawchunks[:0]
  1415  	}
  1416  
  1417  	/* select background colors/images  */
  1418  	var old_clip rect.Rect = out.Clip
  1419  	{
  1420  		var background *nstyle.Item
  1421  		if state&nstyle.WidgetStateActive != 0 {
  1422  			background = &style.Active
  1423  		} else if state&nstyle.WidgetStateHovered != 0 {
  1424  			background = &style.Hover
  1425  		} else {
  1426  			background = &style.Normal
  1427  		}
  1428  
  1429  		/* draw background frame */
  1430  		if background.Type == nstyle.ItemColor {
  1431  			out.FillRect(bounds, style.Rounding, style.BorderColor)
  1432  			out.FillRect(shrinkRect(bounds, style.Border), style.Rounding, background.Data.Color)
  1433  		} else {
  1434  			out.DrawImage(bounds, background.Data.Image)
  1435  		}
  1436  	}
  1437  
  1438  	area.W -= FontWidth(font, "i")
  1439  	clip := unify(old_clip, area)
  1440  	out.PushScissor(clip)
  1441  	/* draw text */
  1442  	var background_color color.RGBA
  1443  	var text_color color.RGBA
  1444  	var sel_background_color color.RGBA
  1445  	var sel_text_color color.RGBA
  1446  	var cursor_color color.RGBA
  1447  	var cursor_text_color color.RGBA
  1448  	var background *nstyle.Item
  1449  
  1450  	/* select correct colors to draw */
  1451  	if state&nstyle.WidgetStateActive != 0 {
  1452  		background = &style.Active
  1453  		text_color = style.TextActive
  1454  		sel_text_color = style.SelectedTextHover
  1455  		sel_background_color = style.SelectedHover
  1456  		cursor_color = style.CursorHover
  1457  		cursor_text_color = style.CursorTextHover
  1458  	} else if state&nstyle.WidgetStateHovered != 0 {
  1459  		background = &style.Hover
  1460  		text_color = style.TextHover
  1461  		sel_text_color = style.SelectedTextHover
  1462  		sel_background_color = style.SelectedHover
  1463  		cursor_text_color = style.CursorTextHover
  1464  		cursor_color = style.CursorHover
  1465  	} else {
  1466  		background = &style.Normal
  1467  		text_color = style.TextNormal
  1468  		sel_text_color = style.SelectedTextNormal
  1469  		sel_background_color = style.SelectedNormal
  1470  		cursor_color = style.CursorNormal
  1471  		cursor_text_color = style.CursorTextNormal
  1472  	}
  1473  
  1474  	if background.Type == nstyle.ItemImage {
  1475  		background_color = color.RGBA{0, 0, 0, 0}
  1476  	} else {
  1477  		background_color = background.Data.Color
  1478  	}
  1479  
  1480  	startPos := image.Point{area.X - edit.Scrollbar.X, area.Y - edit.Scrollbar.Y}
  1481  	pos := startPos
  1482  	x_margin := pos.X
  1483  	if edit.SelectStart == edit.SelectEnd {
  1484  		drawEolCursor := func() {
  1485  			cursor_pos := d.CursorPos
  1486  			/* draw cursor at end of line */
  1487  			var cursor rect.Rect
  1488  			if edit.Flags&EditIbeamCursor != 0 {
  1489  				cursor.W = int(d.Scaling)
  1490  				if cursor.W <= 0 {
  1491  					cursor.W = 1
  1492  				}
  1493  			} else {
  1494  				cursor.W = FontWidth(font, "i")
  1495  			}
  1496  			cursor.H = row_height
  1497  			cursor.X = area.X + cursor_pos.X - edit.Scrollbar.X
  1498  			cursor.Y = area.Y + cursor_pos.Y + row_height/2.0 - cursor.H/2.0
  1499  			cursor.Y -= edit.Scrollbar.Y
  1500  			out.FillRect(cursor, 0, cursor_color)
  1501  		}
  1502  
  1503  		/* no selection so just draw the complete text */
  1504  		pos = edit.editDrawText(out, style, pos, x_margin, edit.Buffer[:edit.Cursor], 0, row_height, font, background_color, text_color, false)
  1505  		d.CursorPos = pos.Sub(startPos)
  1506  		if edit.Active && d.hasInput {
  1507  			if edit.Cursor < len(edit.Buffer) {
  1508  				cursorChar := edit.Buffer[edit.Cursor]
  1509  				if cursorChar == '\n' || cursorChar == '\t' || edit.Flags&EditIbeamCursor != 0 {
  1510  					pos = edit.editDrawText(out, style, pos, x_margin, edit.Buffer[edit.Cursor:edit.Cursor+1], edit.Cursor, row_height, font, background_color, text_color, true)
  1511  					drawEolCursor()
  1512  				} else {
  1513  					pos = edit.editDrawText(out, style, pos, x_margin, edit.Buffer[edit.Cursor:edit.Cursor+1], edit.Cursor, row_height, font, cursor_color, cursor_text_color, true)
  1514  				}
  1515  				pos = edit.editDrawText(out, style, pos, x_margin, edit.Buffer[edit.Cursor+1:], edit.Cursor+1, row_height, font, background_color, text_color, false)
  1516  			} else {
  1517  				drawEolCursor()
  1518  			}
  1519  		} else if edit.Cursor < len(edit.Buffer) {
  1520  			pos = edit.editDrawText(out, style, pos, x_margin, edit.Buffer[edit.Cursor:], edit.Cursor, row_height, font, background_color, text_color, false)
  1521  		}
  1522  	} else {
  1523  		/* edit has selection so draw 1-3 text chunks */
  1524  		if selection_begin > 0 {
  1525  			/* draw unselected text before selection */
  1526  			pos = edit.editDrawText(out, style, pos, x_margin, edit.Buffer[:selection_begin], 0, row_height, font, background_color, text_color, false)
  1527  		}
  1528  
  1529  		if selection_begin == edit.SelectEnd {
  1530  			d.CursorPos = pos.Sub(startPos)
  1531  		}
  1532  
  1533  		pos = edit.editDrawText(out, style, pos, x_margin, edit.Buffer[selection_begin:selection_end], selection_begin, row_height, font, sel_background_color, sel_text_color, true)
  1534  
  1535  		if selection_begin != edit.SelectEnd {
  1536  			d.CursorPos = pos.Sub(startPos)
  1537  		}
  1538  
  1539  		if selection_end < len(edit.Buffer) {
  1540  			pos = edit.editDrawText(out, style, pos, x_margin, edit.Buffer[selection_end:], selection_end, row_height, font, background_color, text_color, false)
  1541  		}
  1542  	}
  1543  	d.TextSize = pos.Sub(startPos)
  1544  
  1545  	// fix rectangles in drawchunks by subtracting area from them
  1546  	for i := range edit.drawchunks {
  1547  		edit.drawchunks[i].X -= area.X
  1548  		edit.drawchunks[i].Y -= area.Y
  1549  	}
  1550  
  1551  	out.PushScissor(old_clip)
  1552  }
  1553  
  1554  func runeSliceEquals(a, b []rune) bool {
  1555  	if len(a) != len(b) {
  1556  		return false
  1557  	}
  1558  	for i := range a {
  1559  		if a[i] != b[i] {
  1560  			return false
  1561  		}
  1562  	}
  1563  	return true
  1564  }
  1565  
  1566  var clipboardModifier = func() gkey.Modifiers {
  1567  	if runtime.GOOS == "darwin" {
  1568  		return gkey.ModMeta
  1569  	}
  1570  	return gkey.ModCtrl
  1571  }()
  1572  
  1573  func (edit *TextEditor) popupFind() {
  1574  	if edit.Flags&EditMultiline == 0 {
  1575  		return
  1576  	}
  1577  	var searchEd TextEditor
  1578  	searchEd.Flags = EditSigEnter | EditClipboard | EditSelectable
  1579  	searchEd.Buffer = append(searchEd.Buffer[:0], edit.needle...)
  1580  	searchEd.SelectStart = 0
  1581  	searchEd.SelectEnd = len(searchEd.Buffer)
  1582  	searchEd.Cursor = searchEd.SelectEnd
  1583  	searchEd.Active = true
  1584  
  1585  	edit.SelectEnd = edit.SelectStart
  1586  	edit.Cursor = edit.SelectStart
  1587  
  1588  	edit.win.Master().PopupOpen("Search...", WindowTitle|WindowNoScrollbar|WindowMovable|WindowBorder|WindowDynamic, rect.Rect{100, 100, 400, 500}, true, func(w *Window) {
  1589  		w.Row(30).Static()
  1590  		w.LayoutFitWidth(0, 30)
  1591  		w.Label("Search: ", "LC")
  1592  		w.LayoutSetWidth(150)
  1593  		ev := searchEd.Edit(w)
  1594  		if ev&EditCommitted != 0 {
  1595  			edit.Active = true
  1596  			w.Close()
  1597  		}
  1598  		w.LayoutSetWidth(100)
  1599  		if w.ButtonText("Done") {
  1600  			edit.Active = true
  1601  			w.Close()
  1602  		}
  1603  		kbd := &w.Input().Keyboard
  1604  		for _, k := range kbd.Keys {
  1605  			switch {
  1606  			case k.Modifiers == clipboardModifier && k.Code == gkey.CodeG:
  1607  				edit.lookForward(true)
  1608  			case k.Modifiers == 0 && k.Code == gkey.CodeEscape:
  1609  				edit.SelectEnd = edit.SelectStart
  1610  				edit.Cursor = edit.SelectStart
  1611  				edit.Active = true
  1612  				w.Close()
  1613  			}
  1614  		}
  1615  		if !runeSliceEquals(searchEd.Buffer, edit.needle) {
  1616  			edit.needle = append(edit.needle[:0], searchEd.Buffer...)
  1617  			edit.lookForward(false)
  1618  		}
  1619  	})
  1620  }
  1621  
  1622  func (edit *TextEditor) lookForward(forceAdvance bool) {
  1623  	if edit.Flags&EditMultiline == 0 {
  1624  		return
  1625  	}
  1626  	if edit.hasSelection() {
  1627  		if forceAdvance {
  1628  			edit.SelectStart = edit.SelectEnd
  1629  		} else {
  1630  			edit.SelectEnd = edit.SelectStart
  1631  		}
  1632  		if edit.SelectEnd >= 0 {
  1633  			edit.Cursor = edit.SelectEnd
  1634  		}
  1635  	}
  1636  	for start := edit.Cursor; start < len(edit.Buffer); start++ {
  1637  		found := true
  1638  		for i := 0; i < len(edit.needle); i++ {
  1639  			if edit.needle[i] != edit.Buffer[start+i] {
  1640  				found = false
  1641  				break
  1642  			}
  1643  		}
  1644  		if found {
  1645  			edit.SelectStart = start
  1646  			edit.SelectEnd = start + len(edit.needle)
  1647  			edit.Cursor = edit.SelectEnd
  1648  			edit.CursorFollow = true
  1649  			return
  1650  		}
  1651  	}
  1652  	edit.SelectStart = 0
  1653  	edit.SelectEnd = 0
  1654  	edit.Cursor = 0
  1655  	edit.CursorFollow = true
  1656  }
  1657  
  1658  // Adds text editor edit to win.
  1659  // Initial contents of the text editor will be set to text. If
  1660  // alwaysSet is specified the contents of the editor will be reset
  1661  // to text.
  1662  func (edit *TextEditor) Edit(win *Window) EditEvents {
  1663  	edit.init(win)
  1664  	if edit.Maxlen > 0 {
  1665  		if len(edit.Buffer) > edit.Maxlen {
  1666  			edit.Buffer = edit.Buffer[:edit.Maxlen]
  1667  		}
  1668  	}
  1669  
  1670  	if edit.Flags&EditNoCursor != 0 {
  1671  		edit.Cursor = len(edit.Buffer)
  1672  	}
  1673  	if edit.Flags&EditSelectable == 0 {
  1674  		edit.SelectStart = edit.Cursor
  1675  		edit.SelectEnd = edit.Cursor
  1676  	}
  1677  
  1678  	var bounds rect.Rect
  1679  
  1680  	style := &edit.win.ctx.Style
  1681  	widget_state, bounds, _ := edit.win.widget()
  1682  	if !widget_state {
  1683  		return 0
  1684  	}
  1685  	in := edit.win.inputMaybe(widget_state)
  1686  
  1687  	var cut, copy, paste bool
  1688  
  1689  	if w := win.ContextualOpen(0, image.Point{}, bounds, nil); w != nil {
  1690  		w.Row(20).Dynamic(1)
  1691  		visible := false
  1692  		if edit.Flags&EditClipboard != 0 {
  1693  			visible = true
  1694  			if w.MenuItem(label.TA("Cut", "LC")) {
  1695  				cut = true
  1696  			}
  1697  			if w.MenuItem(label.TA("Copy", "LC")) {
  1698  				copy = true
  1699  			}
  1700  			if w.MenuItem(label.TA("Paste", "LC")) {
  1701  				paste = true
  1702  			}
  1703  		}
  1704  		if edit.Flags&EditMultiline != 0 {
  1705  			visible = true
  1706  			if w.MenuItem(label.TA("Find...", "LC")) {
  1707  				edit.popupFind()
  1708  			}
  1709  		}
  1710  		if !visible {
  1711  			w.Close()
  1712  		}
  1713  	}
  1714  
  1715  	ev := edit.doEdit(bounds, &style.Edit, in, cut, copy, paste)
  1716  	return ev
  1717  }