gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/widget/index.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package widget
     4  
     5  import (
     6  	"bufio"
     7  	"image"
     8  	"io"
     9  	"math"
    10  	"sort"
    11  
    12  	"gioui.org/text"
    13  	"github.com/go-text/typesetting/segmenter"
    14  	"golang.org/x/image/math/fixed"
    15  )
    16  
    17  type lineInfo struct {
    18  	xOff            fixed.Int26_6
    19  	yOff            int
    20  	width           fixed.Int26_6
    21  	ascent, descent fixed.Int26_6
    22  	glyphs          int
    23  }
    24  
    25  type glyphIndex struct {
    26  	// glyphs holds the glyphs processed.
    27  	glyphs []text.Glyph
    28  	// positions contain all possible caret positions, sorted by rune index.
    29  	positions []combinedPos
    30  	// lines contains metadata about the size and position of each line of
    31  	// text.
    32  	lines []lineInfo
    33  
    34  	// currentLineMin and currentLineMax track the dimensions of the line
    35  	// that is being indexed.
    36  	currentLineMin, currentLineMax fixed.Int26_6
    37  	// currentLineGlyphs tracks how many glyphs are contained within the
    38  	// line that is being indexed.
    39  	currentLineGlyphs int
    40  	// pos tracks attributes of the next valid cursor position within the indexed
    41  	// text.
    42  	pos combinedPos
    43  	// prog tracks the current glyph text progression to detect bidi changes.
    44  	prog text.Flags
    45  	// clusterAdvance accumulates the advances of glyphs in a glyph cluster.
    46  	clusterAdvance fixed.Int26_6
    47  	// truncated indicates that the text was truncated by the shaper.
    48  	truncated bool
    49  	// midCluster tracks whether the next glyph processed is not the first glyph in a
    50  	// cluster.
    51  	midCluster bool
    52  }
    53  
    54  // reset prepares the index for reuse.
    55  func (g *glyphIndex) reset() {
    56  	g.glyphs = g.glyphs[:0]
    57  	g.positions = g.positions[:0]
    58  	g.lines = g.lines[:0]
    59  	g.currentLineMin = 0
    60  	g.currentLineMax = 0
    61  	g.currentLineGlyphs = 0
    62  	g.pos = combinedPos{}
    63  	g.prog = 0
    64  	g.clusterAdvance = 0
    65  	g.truncated = false
    66  	g.midCluster = false
    67  }
    68  
    69  // screenPos represents a character position in text line and column numbers,
    70  // not pixels.
    71  type screenPos struct {
    72  	// col is the column, measured in runes.
    73  	// FIXME: we only ever use col for start or end of lines.
    74  	// We don't need accurate accounting, so can we get rid of it?
    75  	col  int
    76  	line int
    77  }
    78  
    79  // combinedPos is a point in the editor.
    80  type combinedPos struct {
    81  	// runes is the offset in runes.
    82  	runes int
    83  
    84  	lineCol screenPos
    85  
    86  	// Pixel coordinates
    87  	x fixed.Int26_6
    88  	y int
    89  
    90  	ascent, descent fixed.Int26_6
    91  
    92  	// runIndex tracks which run this position is within, counted each time
    93  	// the index processes an end of run marker.
    94  	runIndex int
    95  	// towardOrigin tracks whether this glyph's run is progressing toward the
    96  	// origin or away from it.
    97  	towardOrigin bool
    98  }
    99  
   100  // incrementPosition returns the next position after pos (if any). Pos _must_ be
   101  // an unmodified position acquired from one of the closest* methods. If eof is
   102  // true, there was no next position.
   103  func (g *glyphIndex) incrementPosition(pos combinedPos) (next combinedPos, eof bool) {
   104  	candidate, index := g.closestToRune(pos.runes)
   105  	for candidate != pos && index+1 < len(g.positions) {
   106  		index++
   107  		candidate = g.positions[index]
   108  	}
   109  	if index+1 < len(g.positions) {
   110  		return g.positions[index+1], false
   111  	}
   112  	return candidate, true
   113  }
   114  
   115  func (g *glyphIndex) insertPosition(pos combinedPos) {
   116  	lastIdx := len(g.positions) - 1
   117  	if lastIdx >= 0 {
   118  		lastPos := g.positions[lastIdx]
   119  		if lastPos.runes == pos.runes && (lastPos.y != pos.y || (lastPos.x == pos.x)) {
   120  			// If we insert a consecutive position with the same logical position,
   121  			// overwrite the previous position with the new one.
   122  			g.positions[lastIdx] = pos
   123  			return
   124  		}
   125  	}
   126  	g.positions = append(g.positions, pos)
   127  }
   128  
   129  // Glyph indexes the provided glyph, generating text cursor positions for it.
   130  func (g *glyphIndex) Glyph(gl text.Glyph) {
   131  	g.glyphs = append(g.glyphs, gl)
   132  	g.currentLineGlyphs++
   133  	if len(g.positions) == 0 {
   134  		// First-iteration setup.
   135  		g.currentLineMin = math.MaxInt32
   136  		g.currentLineMax = 0
   137  	}
   138  	if gl.X < g.currentLineMin {
   139  		g.currentLineMin = gl.X
   140  	}
   141  	if end := gl.X + gl.Advance; end > g.currentLineMax {
   142  		g.currentLineMax = end
   143  	}
   144  
   145  	needsNewLine := gl.Flags&text.FlagLineBreak != 0
   146  	needsNewRun := gl.Flags&text.FlagRunBreak != 0
   147  	breaksParagraph := gl.Flags&text.FlagParagraphBreak != 0
   148  	breaksCluster := gl.Flags&text.FlagClusterBreak != 0
   149  	// We should insert new positions if the glyph we're processing terminates
   150  	// a glyph cluster, has nonzero runes, and is not a hard newline.
   151  	insertPositionsWithin := breaksCluster && !breaksParagraph && gl.Runes > 0
   152  
   153  	// Get the text progression/direction right.
   154  	g.prog = gl.Flags & text.FlagTowardOrigin
   155  	g.pos.towardOrigin = g.prog == text.FlagTowardOrigin
   156  	if !g.midCluster {
   157  		// Create the text position prior to the glyph.
   158  		g.pos.x = gl.X
   159  		g.pos.y = int(gl.Y)
   160  		g.pos.ascent = gl.Ascent
   161  		g.pos.descent = gl.Descent
   162  		if g.pos.towardOrigin {
   163  			g.pos.x += gl.Advance
   164  		}
   165  		g.insertPosition(g.pos)
   166  	}
   167  
   168  	g.midCluster = !breaksCluster
   169  
   170  	if breaksParagraph {
   171  		// Paragraph breaking clusters shouldn't have positions generated for both
   172  		// sides of them. They're always zero-width, so doing so would
   173  		// create two visually identical cursor positions. Just reset
   174  		// cluster state, increment by their runes, and move on to the
   175  		// next glyph.
   176  		g.clusterAdvance = 0
   177  		g.pos.runes += int(gl.Runes)
   178  	}
   179  	// Always track the cumulative advance added by the glyph, even if it
   180  	// doesn't terminate a cluster itself.
   181  	g.clusterAdvance += gl.Advance
   182  	if insertPositionsWithin {
   183  		// Construct the text positions _within_ gl.
   184  		g.pos.y = int(gl.Y)
   185  		g.pos.ascent = gl.Ascent
   186  		g.pos.descent = gl.Descent
   187  		width := g.clusterAdvance
   188  		positionCount := int(gl.Runes)
   189  		runesPerPosition := 1
   190  		if gl.Flags&text.FlagTruncator != 0 {
   191  			// Treat the truncator as a single unit that is either selected or not.
   192  			positionCount = 1
   193  			runesPerPosition = int(gl.Runes)
   194  			g.truncated = true
   195  		}
   196  		perRune := width / fixed.Int26_6(positionCount)
   197  		adjust := fixed.Int26_6(0)
   198  		if g.pos.towardOrigin {
   199  			// If RTL, subtract increments from the width of the cluster
   200  			// instead of adding.
   201  			adjust = width
   202  			perRune = -perRune
   203  		}
   204  		for i := 1; i <= positionCount; i++ {
   205  			g.pos.x = gl.X + adjust + perRune*fixed.Int26_6(i)
   206  			g.pos.runes += runesPerPosition
   207  			g.pos.lineCol.col += runesPerPosition
   208  			g.insertPosition(g.pos)
   209  		}
   210  		g.clusterAdvance = 0
   211  	}
   212  	if needsNewRun {
   213  		g.pos.runIndex++
   214  	}
   215  	if needsNewLine {
   216  		g.lines = append(g.lines, lineInfo{
   217  			xOff:    g.currentLineMin,
   218  			yOff:    int(gl.Y),
   219  			width:   g.currentLineMax - g.currentLineMin,
   220  			ascent:  g.positions[len(g.positions)-1].ascent,
   221  			descent: g.positions[len(g.positions)-1].descent,
   222  			glyphs:  g.currentLineGlyphs,
   223  		})
   224  		g.pos.lineCol.line++
   225  		g.pos.lineCol.col = 0
   226  		g.pos.runIndex = 0
   227  		g.currentLineMin = math.MaxInt32
   228  		g.currentLineMax = 0
   229  		g.currentLineGlyphs = 0
   230  	}
   231  }
   232  
   233  func (g *glyphIndex) closestToRune(runeIdx int) (combinedPos, int) {
   234  	if len(g.positions) == 0 {
   235  		return combinedPos{}, 0
   236  	}
   237  	i := sort.Search(len(g.positions), func(i int) bool {
   238  		pos := g.positions[i]
   239  		return pos.runes >= runeIdx
   240  	})
   241  	if i > 0 {
   242  		i--
   243  	}
   244  	closest := g.positions[i]
   245  	closestI := i
   246  	for ; i < len(g.positions); i++ {
   247  		if g.positions[i].runes == runeIdx {
   248  			return g.positions[i], i
   249  		}
   250  	}
   251  	return closest, closestI
   252  }
   253  
   254  func (g *glyphIndex) closestToLineCol(lineCol screenPos) combinedPos {
   255  	if len(g.positions) == 0 {
   256  		return combinedPos{}
   257  	}
   258  	i := sort.Search(len(g.positions), func(i int) bool {
   259  		pos := g.positions[i]
   260  		return pos.lineCol.line > lineCol.line || (pos.lineCol.line == lineCol.line && pos.lineCol.col >= lineCol.col)
   261  	})
   262  	if i > 0 {
   263  		i--
   264  	}
   265  	prior := g.positions[i]
   266  	if i+1 >= len(g.positions) {
   267  		return prior
   268  	}
   269  	next := g.positions[i+1]
   270  	if next.lineCol != lineCol {
   271  		return prior
   272  	}
   273  	return next
   274  }
   275  
   276  func dist(a, b fixed.Int26_6) fixed.Int26_6 {
   277  	if a > b {
   278  		return a - b
   279  	}
   280  	return b - a
   281  }
   282  
   283  func (g *glyphIndex) closestToXY(x fixed.Int26_6, y int) combinedPos {
   284  	if len(g.positions) == 0 {
   285  		return combinedPos{}
   286  	}
   287  	i := sort.Search(len(g.positions), func(i int) bool {
   288  		pos := g.positions[i]
   289  		return pos.y+pos.descent.Round() >= y
   290  	})
   291  	// If no position was greater than the provided Y, the text is too
   292  	// short. Return either the last position or (if there are no
   293  	// positions) the zero position.
   294  	if i == len(g.positions) {
   295  		return g.positions[i-1]
   296  	}
   297  	first := g.positions[i]
   298  	// Find the best X coordinate.
   299  	closest := i
   300  	closestDist := dist(first.x, x)
   301  	line := first.lineCol.line
   302  	// NOTE(whereswaldon): there isn't a simple way to accelerate this. Bidi text means that the x coordinates
   303  	// for positions have no fixed relationship. In the future, we can consider sorting the positions
   304  	// on a line by their x coordinate and caching that. It'll be a one-time O(nlogn) per line, but
   305  	// subsequent uses of this function for that line become O(logn). Right now it's always O(n).
   306  	for i := i + 1; i < len(g.positions) && g.positions[i].lineCol.line == line; i++ {
   307  		candidate := g.positions[i]
   308  		distance := dist(candidate.x, x)
   309  		// If we are *really* close to the current position candidate, just choose it.
   310  		if distance.Round() == 0 {
   311  			return g.positions[i]
   312  		}
   313  		if distance < closestDist {
   314  			closestDist = distance
   315  			closest = i
   316  		}
   317  	}
   318  	return g.positions[closest]
   319  }
   320  
   321  // makeRegion creates a text-aligned rectangle from start to end. The vertical
   322  // dimensions of the rectangle are derived from the provided line's ascent and
   323  // descent, and the y offset of the line's baseline is provided as y.
   324  func makeRegion(line lineInfo, y int, start, end fixed.Int26_6) Region {
   325  	if start > end {
   326  		start, end = end, start
   327  	}
   328  	dotStart := image.Pt(start.Round(), y)
   329  	dotEnd := image.Pt(end.Round(), y)
   330  	return Region{
   331  		Bounds: image.Rectangle{
   332  			Min: dotStart.Sub(image.Point{Y: line.ascent.Ceil()}),
   333  			Max: dotEnd.Add(image.Point{Y: line.descent.Floor()}),
   334  		},
   335  		Baseline: line.descent.Floor(),
   336  	}
   337  }
   338  
   339  // Region describes the position and baseline of an area of interest within
   340  // shaped text.
   341  type Region struct {
   342  	// Bounds is the coordinates of the bounding box relative to the containing
   343  	// widget.
   344  	Bounds image.Rectangle
   345  	// Baseline is the quantity of vertical pixels between the baseline and
   346  	// the bottom of bounds.
   347  	Baseline int
   348  }
   349  
   350  // locate returns highlight regions covering the glyphs that represent the runes in
   351  // [startRune,endRune). If the rects parameter is non-nil, locate will use it to
   352  // return results instead of allocating, provided that there is enough capacity.
   353  // The returned regions have their Bounds specified relative to the provided
   354  // viewport.
   355  func (g *glyphIndex) locate(viewport image.Rectangle, startRune, endRune int, rects []Region) []Region {
   356  	if startRune > endRune {
   357  		startRune, endRune = endRune, startRune
   358  	}
   359  	rects = rects[:0]
   360  	caretStart, _ := g.closestToRune(startRune)
   361  	caretEnd, _ := g.closestToRune(endRune)
   362  
   363  	for lineIdx := caretStart.lineCol.line; lineIdx < len(g.lines); lineIdx++ {
   364  		if lineIdx > caretEnd.lineCol.line {
   365  			break
   366  		}
   367  		pos := g.closestToLineCol(screenPos{line: lineIdx})
   368  		if int(pos.y)+pos.descent.Ceil() < viewport.Min.Y {
   369  			continue
   370  		}
   371  		if int(pos.y)-pos.ascent.Ceil() > viewport.Max.Y {
   372  			break
   373  		}
   374  		line := g.lines[lineIdx]
   375  		if lineIdx > caretStart.lineCol.line && lineIdx < caretEnd.lineCol.line {
   376  			startX := line.xOff
   377  			endX := startX + line.width
   378  			// The entire line is selected.
   379  			rects = append(rects, makeRegion(line, pos.y, startX, endX))
   380  			continue
   381  		}
   382  		selectionStart := caretStart
   383  		selectionEnd := caretEnd
   384  		if lineIdx != caretStart.lineCol.line {
   385  			// This line does not contain the beginning of the selection.
   386  			selectionStart = g.closestToLineCol(screenPos{line: lineIdx})
   387  		}
   388  		if lineIdx != caretEnd.lineCol.line {
   389  			// This line does not contain the end of the selection.
   390  			selectionEnd = g.closestToLineCol(screenPos{line: lineIdx, col: math.MaxInt})
   391  		}
   392  
   393  		var (
   394  			startX, endX fixed.Int26_6
   395  			eof          bool
   396  		)
   397  	lineLoop:
   398  		for !eof {
   399  			startX = selectionStart.x
   400  			if selectionStart.runIndex == selectionEnd.runIndex {
   401  				// Commit selection.
   402  				endX = selectionEnd.x
   403  				rects = append(rects, makeRegion(line, pos.y, startX, endX))
   404  				break
   405  			} else {
   406  				currentDirection := selectionStart.towardOrigin
   407  				previous := selectionStart
   408  			runLoop:
   409  				for !eof {
   410  					// Increment the start position until the next logical run.
   411  					for startRun := selectionStart.runIndex; selectionStart.runIndex == startRun; {
   412  						previous = selectionStart
   413  						selectionStart, eof = g.incrementPosition(selectionStart)
   414  						if eof {
   415  							endX = selectionStart.x
   416  							rects = append(rects, makeRegion(line, pos.y, startX, endX))
   417  							break runLoop
   418  						}
   419  					}
   420  					if selectionStart.towardOrigin != currentDirection {
   421  						endX = previous.x
   422  						rects = append(rects, makeRegion(line, pos.y, startX, endX))
   423  						break
   424  					}
   425  					if selectionStart.runIndex == selectionEnd.runIndex {
   426  						// Commit selection.
   427  						endX = selectionEnd.x
   428  						rects = append(rects, makeRegion(line, pos.y, startX, endX))
   429  						break lineLoop
   430  					}
   431  				}
   432  			}
   433  		}
   434  	}
   435  	for i := range rects {
   436  		rects[i].Bounds = rects[i].Bounds.Sub(viewport.Min)
   437  	}
   438  	return rects
   439  }
   440  
   441  // graphemeReader segments paragraphs of text into grapheme clusters.
   442  type graphemeReader struct {
   443  	segmenter.Segmenter
   444  	graphemes  []int
   445  	paragraph  []rune
   446  	source     io.ReaderAt
   447  	cursor     int64
   448  	reader     *bufio.Reader
   449  	runeOffset int
   450  }
   451  
   452  // SetSource configures the reader to pull from source.
   453  func (p *graphemeReader) SetSource(source io.ReaderAt) {
   454  	p.source = source
   455  	p.cursor = 0
   456  	p.reader = bufio.NewReader(p)
   457  	p.runeOffset = 0
   458  }
   459  
   460  // Read exists to satisfy io.Reader. It should not be directly invoked.
   461  func (p *graphemeReader) Read(b []byte) (int, error) {
   462  	n, err := p.source.ReadAt(b, p.cursor)
   463  	p.cursor += int64(n)
   464  	return n, err
   465  }
   466  
   467  // next decodes one paragraph of rune data.
   468  func (p *graphemeReader) next() ([]rune, bool) {
   469  	p.paragraph = p.paragraph[:0]
   470  	var err error
   471  	var r rune
   472  	for err == nil {
   473  		r, _, err = p.reader.ReadRune()
   474  		if err != nil {
   475  			break
   476  		}
   477  		p.paragraph = append(p.paragraph, r)
   478  		if r == '\n' {
   479  			break
   480  		}
   481  	}
   482  	return p.paragraph, err == nil
   483  }
   484  
   485  // Graphemes will return the next paragraph's grapheme cluster boundaries,
   486  // if any. If it returns an empty slice, there is no more data (all paragraphs
   487  // have been segmented).
   488  func (p *graphemeReader) Graphemes() []int {
   489  	var more bool
   490  	p.graphemes = p.graphemes[:0]
   491  	p.paragraph, more = p.next()
   492  	if len(p.paragraph) == 0 && !more {
   493  		return nil
   494  	}
   495  	p.Segmenter.Init(p.paragraph)
   496  	iter := p.Segmenter.GraphemeIterator()
   497  	if iter.Next() {
   498  		graph := iter.Grapheme()
   499  		p.graphemes = append(p.graphemes,
   500  			p.runeOffset+graph.Offset,
   501  			p.runeOffset+graph.Offset+len(graph.Text),
   502  		)
   503  	}
   504  	for iter.Next() {
   505  		graph := iter.Grapheme()
   506  		p.graphemes = append(p.graphemes, p.runeOffset+graph.Offset+len(graph.Text))
   507  	}
   508  	p.runeOffset += len(p.paragraph)
   509  	return p.graphemes
   510  }