github.com/Seikaijyu/gio@v0.0.1/widget/text.go (about)

     1  package widget
     2  
     3  import (
     4  	"bufio"
     5  	"image"
     6  	"io"
     7  	"math"
     8  	"sort"
     9  	"unicode"
    10  	"unicode/utf8"
    11  
    12  	"github.com/Seikaijyu/gio/f32"
    13  	"github.com/Seikaijyu/gio/font"
    14  	"github.com/Seikaijyu/gio/layout"
    15  	"github.com/Seikaijyu/gio/op"
    16  	"github.com/Seikaijyu/gio/op/clip"
    17  	"github.com/Seikaijyu/gio/op/paint"
    18  	"github.com/Seikaijyu/gio/text"
    19  	"github.com/Seikaijyu/gio/unit"
    20  	"golang.org/x/exp/slices"
    21  	"golang.org/x/image/math/fixed"
    22  )
    23  
    24  // textSource provides text data for use in widgets. If the underlying data type
    25  // can fail due to I/O errors, it is the responsibility of that type to provide
    26  // its own mechanism to surface and handle those errors. They will not always
    27  // be returned by widgets using these functions.
    28  type textSource interface {
    29  	io.ReaderAt
    30  	// Size returns the total length of the data in bytes.
    31  	Size() int64
    32  	// Changed returns whether the contents have changed since the last call
    33  	// to Changed.
    34  	Changed() bool
    35  	// ReplaceRunes replaces runeCount runes starting at byteOffset within the
    36  	// data with the provided string. Implementations of read-only text sources
    37  	// are free to make this a no-op.
    38  	ReplaceRunes(byteOffset int64, runeCount int64, replacement string)
    39  }
    40  
    41  // textView provides efficient shaping and indexing of interactive text. When provided
    42  // with a TextSource, textView will shape and cache the runes within that source.
    43  // It provides methods for configuring a viewport onto the shaped text which can
    44  // be scrolled, and for configuring and drawing text selection boxes.
    45  type textView struct {
    46  	Alignment text.Alignment
    47  	// LineHeight controls the distance between the baselines of lines of text.
    48  	// If zero, a sensible default will be used.
    49  	LineHeight unit.Sp
    50  	// LineHeightScale applies a scaling factor to the LineHeight. If zero, a
    51  	// sensible default will be used.
    52  	LineHeightScale float32
    53  	// SingleLine forces the text to stay on a single line.
    54  	// SingleLine also sets the scrolling direction to
    55  	// horizontal.
    56  	SingleLine bool
    57  	// MaxLines limits the shaped text to a specific quantity of shaped lines.
    58  	MaxLines int
    59  	// Truncator is the text that will be shown at the end of the final
    60  	// line if MaxLines is exceeded. Defaults to "…" if empty.
    61  	Truncator string
    62  	// WrapPolicy configures how displayed text will be broken into lines.
    63  	WrapPolicy text.WrapPolicy
    64  	// Mask replaces the visual display of each rune in the contents with the given rune.
    65  	// Newline characters are not masked. When non-zero, the unmasked contents
    66  	// are accessed by Len, Text, and SetText.
    67  	Mask rune
    68  
    69  	params     text.Parameters
    70  	shaper     *text.Shaper
    71  	seekCursor int64
    72  	rr         textSource
    73  	maskReader maskReader
    74  	// graphemes tracks the indices of grapheme cluster boundaries within rr.
    75  	graphemes []int
    76  	// paragraphReader is used to populate graphemes.
    77  	paragraphReader graphemeReader
    78  	lastMask        rune
    79  	viewSize        image.Point
    80  	valid           bool
    81  	regions         []Region
    82  	dims            layout.Dimensions
    83  
    84  	// offIndex is an index of rune index to byte offsets.
    85  	offIndex []offEntry
    86  
    87  	index glyphIndex
    88  
    89  	caret struct {
    90  		// xoff is the offset to the current position when moving between lines.
    91  		xoff fixed.Int26_6
    92  		// start is the current caret position in runes, and also the start position of
    93  		// selected text. end is the end position of selected text. If start
    94  		// == end, then there's no selection. Note that it's possible (and
    95  		// common) that the caret (start) is after the end, e.g. after
    96  		// Shift-DownArrow.
    97  		start int
    98  		end   int
    99  	}
   100  
   101  	scrollOff image.Point
   102  }
   103  
   104  func (e *textView) Changed() bool {
   105  	return e.rr.Changed()
   106  }
   107  
   108  // Dimensions returns the dimensions of the visible text.
   109  func (e *textView) Dimensions() layout.Dimensions {
   110  	basePos := e.dims.Size.Y - e.dims.Baseline
   111  	return layout.Dimensions{Size: e.viewSize, Baseline: e.viewSize.Y - basePos}
   112  }
   113  
   114  // FullDimensions returns the dimensions of all shaped text, including
   115  // text that isn't visible within the current viewport.
   116  func (e *textView) FullDimensions() layout.Dimensions {
   117  	return e.dims
   118  }
   119  
   120  // SetSource initializes the underlying data source for the Text. This
   121  // must be done before invoking any other methods on Text.
   122  func (e *textView) SetSource(source textSource) {
   123  	e.rr = source
   124  	e.invalidate()
   125  	e.seekCursor = 0
   126  }
   127  
   128  // ReadRuneAt reads the rune starting at the given byte offset, if any.
   129  func (e *textView) ReadRuneAt(off int64) (rune, int, error) {
   130  	var buf [utf8.UTFMax]byte
   131  	b := buf[:]
   132  	n, err := e.rr.ReadAt(b, off)
   133  	b = b[:n]
   134  	r, s := utf8.DecodeRune(b)
   135  	return r, s, err
   136  }
   137  
   138  // ReadRuneAt reads the run prior to the given byte offset, if any.
   139  func (e *textView) ReadRuneBefore(off int64) (rune, int, error) {
   140  	var buf [utf8.UTFMax]byte
   141  	b := buf[:]
   142  	if off < utf8.UTFMax {
   143  		b = b[:off]
   144  		off = 0
   145  	} else {
   146  		off -= utf8.UTFMax
   147  	}
   148  	n, err := e.rr.ReadAt(b, off)
   149  	b = b[:n]
   150  	r, s := utf8.DecodeLastRune(b)
   151  	return r, s, err
   152  }
   153  
   154  func (e *textView) makeValid() {
   155  	if e.valid {
   156  		return
   157  	}
   158  	e.layoutText(e.shaper)
   159  	e.valid = true
   160  }
   161  
   162  func (e *textView) closestToRune(runeIdx int) combinedPos {
   163  	e.makeValid()
   164  	pos, _ := e.index.closestToRune(runeIdx)
   165  	return pos
   166  }
   167  
   168  func (e *textView) closestToLineCol(line, col int) combinedPos {
   169  	e.makeValid()
   170  	return e.index.closestToLineCol(screenPos{line: line, col: col})
   171  }
   172  
   173  func (e *textView) closestToXY(x fixed.Int26_6, y int) combinedPos {
   174  	e.makeValid()
   175  	return e.index.closestToXY(x, y)
   176  }
   177  
   178  func (e *textView) closestToXYGraphemes(x fixed.Int26_6, y int) combinedPos {
   179  	// Find the closest existing rune position to the provided coordinates.
   180  	pos := e.closestToXY(x, y)
   181  	// Resolve cluster boundaries on either side of the rune position.
   182  	firstOption := e.moveByGraphemes(pos.runes, 0)
   183  	distance := 1
   184  	if firstOption > pos.runes {
   185  		distance = -1
   186  	}
   187  	secondOption := e.moveByGraphemes(firstOption, distance)
   188  	// Choose the closest grapheme cluster boundary to the desired point.
   189  	first := e.closestToRune(firstOption)
   190  	firstDist := absFixed(first.x - x)
   191  	second := e.closestToRune(secondOption)
   192  	secondDist := absFixed(second.x - x)
   193  	if firstDist > secondDist {
   194  		return second
   195  	} else {
   196  		return first
   197  	}
   198  }
   199  
   200  func absFixed(i fixed.Int26_6) fixed.Int26_6 {
   201  	if i < 0 {
   202  		return -i
   203  	}
   204  	return i
   205  }
   206  
   207  // MaxLines moves the cursor the specified number of lines vertically, ensuring
   208  // that the resulting position is aligned to a grapheme cluster.
   209  func (e *textView) MoveLines(distance int, selAct selectionAction) {
   210  	caretStart := e.closestToRune(e.caret.start)
   211  	x := caretStart.x + e.caret.xoff
   212  	// Seek to line.
   213  	pos := e.closestToLineCol(caretStart.lineCol.line+distance, 0)
   214  	pos = e.closestToXYGraphemes(x, pos.y)
   215  	e.caret.start = pos.runes
   216  	e.caret.xoff = x - pos.x
   217  	e.updateSelection(selAct)
   218  }
   219  
   220  // calculateViewSize determines the size of the current visible content,
   221  // ensuring that even if there is no text content, some space is reserved
   222  // for the caret.
   223  func (e *textView) calculateViewSize(gtx layout.Context) image.Point {
   224  	base := e.dims.Size
   225  	if caretWidth := e.caretWidth(gtx); base.X < caretWidth {
   226  		base.X = caretWidth
   227  	}
   228  	return gtx.Constraints.Constrain(base)
   229  }
   230  
   231  // Layout the text, reshaping it as necessary.
   232  func (e *textView) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp) {
   233  	if e.params.Locale != gtx.Locale {
   234  		e.params.Locale = gtx.Locale
   235  		e.invalidate()
   236  	}
   237  	textSize := fixed.I(gtx.Sp(size))
   238  	if e.params.Font != font || e.params.PxPerEm != textSize {
   239  		e.invalidate()
   240  		e.params.Font = font
   241  		e.params.PxPerEm = textSize
   242  	}
   243  	maxWidth := gtx.Constraints.Max.X
   244  	if e.SingleLine {
   245  		maxWidth = math.MaxInt
   246  	}
   247  	minWidth := gtx.Constraints.Min.X
   248  	if maxWidth != e.params.MaxWidth {
   249  		e.params.MaxWidth = maxWidth
   250  		e.invalidate()
   251  	}
   252  	if minWidth != e.params.MinWidth {
   253  		e.params.MinWidth = minWidth
   254  		e.invalidate()
   255  	}
   256  	if lt != e.shaper {
   257  		e.shaper = lt
   258  		e.invalidate()
   259  	}
   260  	if e.Mask != e.lastMask {
   261  		e.lastMask = e.Mask
   262  		e.invalidate()
   263  	}
   264  	if e.Alignment != e.params.Alignment {
   265  		e.params.Alignment = e.Alignment
   266  		e.invalidate()
   267  	}
   268  	if e.Truncator != e.params.Truncator {
   269  		e.params.Truncator = e.Truncator
   270  		e.invalidate()
   271  	}
   272  	if e.MaxLines != e.params.MaxLines {
   273  		e.params.MaxLines = e.MaxLines
   274  		e.invalidate()
   275  	}
   276  	if e.WrapPolicy != e.params.WrapPolicy {
   277  		e.params.WrapPolicy = e.WrapPolicy
   278  		e.invalidate()
   279  	}
   280  	if lh := fixed.I(gtx.Sp(e.LineHeight)); lh != e.params.LineHeight {
   281  		e.params.LineHeight = lh
   282  		e.invalidate()
   283  	}
   284  	if e.LineHeightScale != e.params.LineHeightScale {
   285  		e.params.LineHeightScale = e.LineHeightScale
   286  		e.invalidate()
   287  	}
   288  
   289  	e.makeValid()
   290  
   291  	if viewSize := e.calculateViewSize(gtx); viewSize != e.viewSize {
   292  		e.viewSize = viewSize
   293  		e.invalidate()
   294  	}
   295  	e.makeValid()
   296  }
   297  
   298  // PaintSelection clips and paints the visible text selection rectangles using
   299  // the provided material to fill the rectangles.
   300  func (e *textView) PaintSelection(gtx layout.Context, material op.CallOp) {
   301  	localViewport := image.Rectangle{Max: e.viewSize}
   302  	docViewport := image.Rectangle{Max: e.viewSize}.Add(e.scrollOff)
   303  	defer clip.Rect(localViewport).Push(gtx.Ops).Pop()
   304  	e.regions = e.index.locate(docViewport, e.caret.start, e.caret.end, e.regions)
   305  	for _, region := range e.regions {
   306  		area := clip.Rect(region.Bounds).Push(gtx.Ops)
   307  		material.Add(gtx.Ops)
   308  		paint.PaintOp{}.Add(gtx.Ops)
   309  		area.Pop()
   310  	}
   311  }
   312  
   313  // PaintText clips and paints the visible text glyph outlines using the provided
   314  // material to fill the glyphs.
   315  func (e *textView) PaintText(gtx layout.Context, material op.CallOp) {
   316  	m := op.Record(gtx.Ops)
   317  	viewport := image.Rectangle{
   318  		Min: e.scrollOff,
   319  		Max: e.viewSize.Add(e.scrollOff),
   320  	}
   321  	it := textIterator{
   322  		viewport: viewport,
   323  		material: material,
   324  	}
   325  
   326  	startGlyph := 0
   327  	for _, line := range e.index.lines {
   328  		if line.descent.Ceil()+line.yOff >= viewport.Min.Y {
   329  			break
   330  		}
   331  		startGlyph += line.glyphs
   332  	}
   333  	var glyphs [32]text.Glyph
   334  	line := glyphs[:0]
   335  	for _, g := range e.index.glyphs[startGlyph:] {
   336  		var ok bool
   337  		if line, ok = it.paintGlyph(gtx, e.shaper, g, line); !ok {
   338  			break
   339  		}
   340  	}
   341  
   342  	call := m.Stop()
   343  	viewport.Min = viewport.Min.Add(it.padding.Min)
   344  	viewport.Max = viewport.Max.Add(it.padding.Max)
   345  	defer clip.Rect(viewport.Sub(e.scrollOff)).Push(gtx.Ops).Pop()
   346  	call.Add(gtx.Ops)
   347  }
   348  
   349  // caretWidth returns the width occupied by the caret for the current
   350  // gtx.
   351  func (e *textView) caretWidth(gtx layout.Context) int {
   352  	carWidth2 := gtx.Dp(1) / 2
   353  	if carWidth2 < 1 {
   354  		carWidth2 = 1
   355  	}
   356  	return carWidth2
   357  }
   358  
   359  // PaintCaret clips and paints the caret rectangle, adding material immediately
   360  // before painting to set the appropriate paint material.
   361  func (e *textView) PaintCaret(gtx layout.Context, material op.CallOp) {
   362  	carWidth2 := e.caretWidth(gtx)
   363  	caretPos, carAsc, carDesc := e.CaretInfo()
   364  
   365  	carRect := image.Rectangle{
   366  		Min: caretPos.Sub(image.Pt(carWidth2, carAsc)),
   367  		Max: caretPos.Add(image.Pt(carWidth2, carDesc)),
   368  	}
   369  	cl := image.Rectangle{Max: e.viewSize}
   370  	carRect = cl.Intersect(carRect)
   371  	if !carRect.Empty() {
   372  		defer clip.Rect(carRect).Push(gtx.Ops).Pop()
   373  		material.Add(gtx.Ops)
   374  		paint.PaintOp{}.Add(gtx.Ops)
   375  	}
   376  }
   377  
   378  func (e *textView) CaretInfo() (pos image.Point, ascent, descent int) {
   379  	caretStart := e.closestToRune(e.caret.start)
   380  
   381  	ascent = caretStart.ascent.Ceil()
   382  	descent = caretStart.descent.Ceil()
   383  
   384  	pos = image.Point{
   385  		X: caretStart.x.Round(),
   386  		Y: caretStart.y,
   387  	}
   388  	pos = pos.Sub(e.scrollOff)
   389  	return
   390  }
   391  
   392  // ByteOffset returns the start byte of the rune at the given
   393  // rune offset, clamped to the size of the text.
   394  func (e *textView) ByteOffset(runeOffset int) int64 {
   395  	return int64(e.runeOffset(e.closestToRune(runeOffset).runes))
   396  }
   397  
   398  // Len is the length of the editor contents, in runes.
   399  func (e *textView) Len() int {
   400  	e.makeValid()
   401  	return e.closestToRune(math.MaxInt).runes
   402  }
   403  
   404  // Text returns the contents of the editor. If the provided buf is large enough, it will
   405  // be filled and returned. Otherwise a new buffer will be allocated.
   406  // Callers can guarantee that buf is large enough by giving it capacity e.Len()*utf8.UTFMax.
   407  func (e *textView) Text(buf []byte) []byte {
   408  	size := e.rr.Size()
   409  	if cap(buf) < int(size) {
   410  		buf = make([]byte, size)
   411  	}
   412  	buf = buf[:size]
   413  	e.Seek(0, io.SeekStart)
   414  	n, _ := io.ReadFull(e, buf)
   415  	buf = buf[:n]
   416  	return buf
   417  }
   418  
   419  func (e *textView) ScrollBounds() image.Rectangle {
   420  	var b image.Rectangle
   421  	if e.SingleLine {
   422  		if len(e.index.lines) > 0 {
   423  			line := e.index.lines[0]
   424  			b.Min.X = line.xOff.Floor()
   425  			if b.Min.X > 0 {
   426  				b.Min.X = 0
   427  			}
   428  		}
   429  		b.Max.X = e.dims.Size.X + b.Min.X - e.viewSize.X
   430  	} else {
   431  		b.Max.Y = e.dims.Size.Y - e.viewSize.Y
   432  	}
   433  	return b
   434  }
   435  
   436  func (e *textView) ScrollRel(dx, dy int) {
   437  	e.scrollAbs(e.scrollOff.X+dx, e.scrollOff.Y+dy)
   438  }
   439  
   440  // ScrollOff returns the scroll offset of the text viewport.
   441  func (e *textView) ScrollOff() image.Point {
   442  	return e.scrollOff
   443  }
   444  
   445  func (e *textView) scrollAbs(x, y int) {
   446  	e.scrollOff.X = x
   447  	e.scrollOff.Y = y
   448  	b := e.ScrollBounds()
   449  	if e.scrollOff.X > b.Max.X {
   450  		e.scrollOff.X = b.Max.X
   451  	}
   452  	if e.scrollOff.X < b.Min.X {
   453  		e.scrollOff.X = b.Min.X
   454  	}
   455  	if e.scrollOff.Y > b.Max.Y {
   456  		e.scrollOff.Y = b.Max.Y
   457  	}
   458  	if e.scrollOff.Y < b.Min.Y {
   459  		e.scrollOff.Y = b.Min.Y
   460  	}
   461  }
   462  
   463  // MoveCoord moves the caret to the position closest to the provided
   464  // point that is aligned to a grapheme cluster boundary.
   465  func (e *textView) MoveCoord(pos image.Point) {
   466  	x := fixed.I(pos.X + e.scrollOff.X)
   467  	y := pos.Y + e.scrollOff.Y
   468  	e.caret.start = e.closestToXYGraphemes(x, y).runes
   469  	e.caret.xoff = 0
   470  }
   471  
   472  // Truncated returns whether the text in the textView is currently
   473  // truncated due to a restriction on the number of lines.
   474  func (e *textView) Truncated() bool {
   475  	return e.index.truncated
   476  }
   477  
   478  func (e *textView) layoutText(lt *text.Shaper) {
   479  	e.Seek(0, io.SeekStart)
   480  	var r io.Reader = e
   481  	if e.Mask != 0 {
   482  		e.maskReader.Reset(e, e.Mask)
   483  		r = &e.maskReader
   484  	}
   485  	e.index.reset()
   486  	it := textIterator{viewport: image.Rectangle{Max: image.Point{X: math.MaxInt, Y: math.MaxInt}}}
   487  	if lt != nil {
   488  		lt.Layout(e.params, r)
   489  		for {
   490  			g, ok := lt.NextGlyph()
   491  			if !it.processGlyph(g, ok) {
   492  				break
   493  			}
   494  			e.index.Glyph(g)
   495  		}
   496  	} else {
   497  		// Make a fake glyph for every rune in the reader.
   498  		b := bufio.NewReader(r)
   499  		for _, _, err := b.ReadRune(); err != io.EOF; _, _, err = b.ReadRune() {
   500  			g := text.Glyph{Runes: 1, Flags: text.FlagClusterBreak}
   501  			_ = it.processGlyph(g, true)
   502  			e.index.Glyph(g)
   503  		}
   504  	}
   505  	e.paragraphReader.SetSource(e.rr)
   506  	e.graphemes = e.graphemes[:0]
   507  	for g := e.paragraphReader.Graphemes(); len(g) > 0; g = e.paragraphReader.Graphemes() {
   508  		if len(e.graphemes) > 0 && g[0] == e.graphemes[len(e.graphemes)-1] {
   509  			g = g[1:]
   510  		}
   511  		e.graphemes = append(e.graphemes, g...)
   512  	}
   513  	dims := layout.Dimensions{Size: it.bounds.Size()}
   514  	dims.Baseline = dims.Size.Y - it.baseline
   515  	e.dims = dims
   516  }
   517  
   518  // CaretPos returns the line & column numbers of the caret.
   519  func (e *textView) CaretPos() (line, col int) {
   520  	pos := e.closestToRune(e.caret.start)
   521  	return pos.lineCol.line, pos.lineCol.col
   522  }
   523  
   524  // CaretCoords returns the coordinates of the caret, relative to the
   525  // editor itself.
   526  func (e *textView) CaretCoords() f32.Point {
   527  	pos := e.closestToRune(e.caret.start)
   528  	return f32.Pt(float32(pos.x)/64-float32(e.scrollOff.X), float32(pos.y-e.scrollOff.Y))
   529  }
   530  
   531  // indexRune returns the latest rune index and byte offset no later than r.
   532  func (e *textView) indexRune(r int) offEntry {
   533  	// Initialize index.
   534  	if len(e.offIndex) == 0 {
   535  		e.offIndex = append(e.offIndex, offEntry{})
   536  	}
   537  	i := sort.Search(len(e.offIndex), func(i int) bool {
   538  		entry := e.offIndex[i]
   539  		return entry.runes >= r
   540  	})
   541  	// Return the entry guaranteed to be less than or equal to r.
   542  	if i > 0 {
   543  		i--
   544  	}
   545  	return e.offIndex[i]
   546  }
   547  
   548  // runeOffset returns the byte offset into e.rr of the r'th rune.
   549  // r must be a valid rune index, usually returned by closestPosition.
   550  func (e *textView) runeOffset(r int) int {
   551  	const runesPerIndexEntry = 50
   552  	entry := e.indexRune(r)
   553  	lastEntry := e.offIndex[len(e.offIndex)-1].runes
   554  	for entry.runes < r {
   555  		if entry.runes > lastEntry && entry.runes%runesPerIndexEntry == runesPerIndexEntry-1 {
   556  			e.offIndex = append(e.offIndex, entry)
   557  		}
   558  		_, s, _ := e.ReadRuneAt(int64(entry.bytes))
   559  		entry.bytes += s
   560  		entry.runes++
   561  	}
   562  	return entry.bytes
   563  }
   564  
   565  func (e *textView) invalidate() {
   566  	e.offIndex = e.offIndex[:0]
   567  	e.valid = false
   568  }
   569  
   570  // Replace the text between start and end with s. Indices are in runes.
   571  // It returns the number of runes inserted.
   572  func (e *textView) Replace(start, end int, s string) int {
   573  	if start > end {
   574  		start, end = end, start
   575  	}
   576  	startPos := e.closestToRune(start)
   577  	endPos := e.closestToRune(end)
   578  	startOff := e.runeOffset(startPos.runes)
   579  	replaceSize := endPos.runes - startPos.runes
   580  	sc := utf8.RuneCountInString(s)
   581  	newEnd := startPos.runes + sc
   582  
   583  	e.rr.ReplaceRunes(int64(startOff), int64(replaceSize), s)
   584  	adjust := func(pos int) int {
   585  		switch {
   586  		case newEnd < pos && pos <= endPos.runes:
   587  			pos = newEnd
   588  		case endPos.runes < pos:
   589  			diff := newEnd - endPos.runes
   590  			pos = pos + diff
   591  		}
   592  		return pos
   593  	}
   594  	e.caret.start = adjust(e.caret.start)
   595  	e.caret.end = adjust(e.caret.end)
   596  	e.invalidate()
   597  	return sc
   598  }
   599  
   600  // MovePages moves the caret position by vertical pages of text, ensuring that
   601  // the final position is aligned to a grapheme cluster boundary.
   602  func (e *textView) MovePages(pages int, selAct selectionAction) {
   603  	caret := e.closestToRune(e.caret.start)
   604  	x := caret.x + e.caret.xoff
   605  	y := caret.y + pages*e.viewSize.Y
   606  	pos := e.closestToXYGraphemes(x, y)
   607  	e.caret.start = pos.runes
   608  	e.caret.xoff = x - pos.x
   609  	e.updateSelection(selAct)
   610  }
   611  
   612  // moveByGraphemes returns the rune index resulting from moving the
   613  // specified number of grapheme clusters from startRuneidx.
   614  func (e *textView) moveByGraphemes(startRuneidx, graphemes int) int {
   615  	if len(e.graphemes) == 0 {
   616  		return startRuneidx
   617  	}
   618  	startGraphemeIdx, _ := slices.BinarySearch(e.graphemes, startRuneidx)
   619  	startGraphemeIdx = max(startGraphemeIdx+graphemes, 0)
   620  	startGraphemeIdx = min(startGraphemeIdx, len(e.graphemes)-1)
   621  	startRuneIdx := e.graphemes[startGraphemeIdx]
   622  	return e.closestToRune(startRuneIdx).runes
   623  }
   624  
   625  // clampCursorToGraphemes ensures that the final start/end positions of
   626  // the cursor are on grapheme cluster boundaries.
   627  func (e *textView) clampCursorToGraphemes() {
   628  	e.caret.start = e.moveByGraphemes(e.caret.start, 0)
   629  	e.caret.end = e.moveByGraphemes(e.caret.end, 0)
   630  }
   631  
   632  // MoveCaret moves the caret (aka selection start) and the selection end
   633  // relative to their current positions. Positive distances moves forward,
   634  // negative distances moves backward. Distances are in grapheme clusters which
   635  // better match the expectations of users than runes.
   636  func (e *textView) MoveCaret(startDelta, endDelta int) {
   637  	e.caret.xoff = 0
   638  	e.caret.start = e.moveByGraphemes(e.caret.start, startDelta)
   639  	e.caret.end = e.moveByGraphemes(e.caret.end, endDelta)
   640  }
   641  
   642  // MoveStart moves the caret to the start of the current line, ensuring that the resulting
   643  // cursor position is on a grapheme cluster boundary.
   644  func (e *textView) MoveStart(selAct selectionAction) {
   645  	caret := e.closestToRune(e.caret.start)
   646  	caret = e.closestToLineCol(caret.lineCol.line, 0)
   647  	e.caret.start = caret.runes
   648  	e.caret.xoff = -caret.x
   649  	e.updateSelection(selAct)
   650  	e.clampCursorToGraphemes()
   651  }
   652  
   653  // MoveEnd moves the caret to the end of the current line, ensuring that the resulting
   654  // cursor position is on a grapheme cluster boundary.
   655  func (e *textView) MoveEnd(selAct selectionAction) {
   656  	caret := e.closestToRune(e.caret.start)
   657  	caret = e.closestToLineCol(caret.lineCol.line, math.MaxInt)
   658  	e.caret.start = caret.runes
   659  	e.caret.xoff = fixed.I(e.params.MaxWidth) - caret.x
   660  	e.updateSelection(selAct)
   661  	e.clampCursorToGraphemes()
   662  }
   663  
   664  // MoveWord moves the caret to the next word in the specified direction.
   665  // Positive is forward, negative is backward.
   666  // Absolute values greater than one will skip that many words.
   667  // The final caret position will be aligned to a grapheme cluster boundary.
   668  // BUG(whereswaldon): this method's definition of a "word" is currently
   669  // whitespace-delimited. Languages that do not use whitespace to delimit
   670  // words will experience counter-intuitive behavior when navigating by
   671  // word.
   672  func (e *textView) MoveWord(distance int, selAct selectionAction) {
   673  	// split the distance information into constituent parts to be
   674  	// used independently.
   675  	words, direction := distance, 1
   676  	if distance < 0 {
   677  		words, direction = distance*-1, -1
   678  	}
   679  	// atEnd if caret is at either side of the buffer.
   680  	caret := e.closestToRune(e.caret.start)
   681  	atEnd := func() bool {
   682  		return caret.runes == 0 || caret.runes == e.Len()
   683  	}
   684  	// next returns the appropriate rune given the direction.
   685  	next := func() (r rune) {
   686  		off := e.runeOffset(caret.runes)
   687  		if direction < 0 {
   688  			r, _, _ = e.ReadRuneBefore(int64(off))
   689  		} else {
   690  			r, _, _ = e.ReadRuneAt(int64(off))
   691  		}
   692  		return r
   693  	}
   694  	for ii := 0; ii < words; ii++ {
   695  		for r := next(); unicode.IsSpace(r) && !atEnd(); r = next() {
   696  			e.MoveCaret(direction, 0)
   697  			caret = e.closestToRune(e.caret.start)
   698  		}
   699  		e.MoveCaret(direction, 0)
   700  		caret = e.closestToRune(e.caret.start)
   701  		for r := next(); !unicode.IsSpace(r) && !atEnd(); r = next() {
   702  			e.MoveCaret(direction, 0)
   703  			caret = e.closestToRune(e.caret.start)
   704  		}
   705  	}
   706  	e.updateSelection(selAct)
   707  	e.clampCursorToGraphemes()
   708  }
   709  
   710  func (e *textView) ScrollToCaret() {
   711  	caret := e.closestToRune(e.caret.start)
   712  	if e.SingleLine {
   713  		var dist int
   714  		if d := caret.x.Floor() - e.scrollOff.X; d < 0 {
   715  			dist = d
   716  		} else if d := caret.x.Ceil() - (e.scrollOff.X + e.viewSize.X); d > 0 {
   717  			dist = d
   718  		}
   719  		e.ScrollRel(dist, 0)
   720  	} else {
   721  		miny := caret.y - caret.ascent.Ceil()
   722  		maxy := caret.y + caret.descent.Ceil()
   723  		var dist int
   724  		if d := miny - e.scrollOff.Y; d < 0 {
   725  			dist = d
   726  		} else if d := maxy - (e.scrollOff.Y + e.viewSize.Y); d > 0 {
   727  			dist = d
   728  		}
   729  		e.ScrollRel(0, dist)
   730  	}
   731  }
   732  
   733  // SelectionLen returns the length of the selection, in runes; it is
   734  // equivalent to utf8.RuneCountInString(e.SelectedText()).
   735  func (e *textView) SelectionLen() int {
   736  	return abs(e.caret.start - e.caret.end)
   737  }
   738  
   739  // Selection returns the start and end of the selection, as rune offsets.
   740  // start can be > end.
   741  func (e *textView) Selection() (start, end int) {
   742  	return e.caret.start, e.caret.end
   743  }
   744  
   745  // SetCaret moves the caret to start, and sets the selection end to end. Then
   746  // the two ends are clamped to the nearest grapheme cluster boundary. start
   747  // and end are in runes, and represent offsets into the editor text.
   748  func (e *textView) SetCaret(start, end int) {
   749  	e.caret.start = e.closestToRune(start).runes
   750  	e.caret.end = e.closestToRune(end).runes
   751  	e.clampCursorToGraphemes()
   752  }
   753  
   754  // SelectedText returns the currently selected text (if any) from the editor,
   755  // filling the provided byte slice if it is large enough or allocating and
   756  // returning a new byte slice if the provided one is insufficient.
   757  // Callers can guarantee that the buf is large enough by providing a buffer
   758  // with capacity e.SelectionLen()*utf8.UTFMax.
   759  func (e *textView) SelectedText(buf []byte) []byte {
   760  	startOff := e.runeOffset(e.caret.start)
   761  	endOff := e.runeOffset(e.caret.end)
   762  	start := min(startOff, endOff)
   763  	end := max(startOff, endOff)
   764  	if cap(buf) < end-start {
   765  		buf = make([]byte, end-start)
   766  	}
   767  	buf = buf[:end-start]
   768  	n, _ := e.rr.ReadAt(buf, int64(start))
   769  	// There is no way to reasonably handle a read error here. We rely upon
   770  	// implementations of textSource to provide other ways to signal errors
   771  	// if the user cares about that, and here we use whatever data we were
   772  	// able to read.
   773  	return buf[:n]
   774  }
   775  
   776  func (e *textView) updateSelection(selAct selectionAction) {
   777  	if selAct == selectionClear {
   778  		e.ClearSelection()
   779  	}
   780  }
   781  
   782  // ClearSelection clears the selection, by setting the selection end equal to
   783  // the selection start.
   784  func (e *textView) ClearSelection() {
   785  	e.caret.end = e.caret.start
   786  }
   787  
   788  // WriteTo implements io.WriterTo.
   789  func (e *textView) WriteTo(w io.Writer) (int64, error) {
   790  	e.Seek(0, io.SeekStart)
   791  	return io.Copy(w, struct{ io.Reader }{e})
   792  }
   793  
   794  // Seek implements io.Seeker.
   795  func (e *textView) Seek(offset int64, whence int) (int64, error) {
   796  	switch whence {
   797  	case io.SeekStart:
   798  		e.seekCursor = offset
   799  	case io.SeekCurrent:
   800  		e.seekCursor += offset
   801  	case io.SeekEnd:
   802  		e.seekCursor = e.rr.Size() + offset
   803  	}
   804  	return e.seekCursor, nil
   805  }
   806  
   807  // Read implements io.Reader.
   808  func (e *textView) Read(p []byte) (int, error) {
   809  	n, err := e.rr.ReadAt(p, e.seekCursor)
   810  	e.seekCursor += int64(n)
   811  	return n, err
   812  }
   813  
   814  // ReadAt implements io.ReaderAt.
   815  func (e *textView) ReadAt(p []byte, offset int64) (int, error) {
   816  	return e.rr.ReadAt(p, offset)
   817  }
   818  
   819  // Regions returns visible regions covering the rune range [start,end).
   820  func (e *textView) Regions(start, end int, regions []Region) []Region {
   821  	viewport := image.Rectangle{
   822  		Min: e.scrollOff,
   823  		Max: e.viewSize.Add(e.scrollOff),
   824  	}
   825  	return e.index.locate(viewport, start, end, regions)
   826  }