123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 |
- // SPDX-License-Identifier: Unlicense OR MIT
- package widget
- import (
- "bufio"
- "image"
- "io"
- "math"
- "sort"
- "gioui.org/text"
- "github.com/go-text/typesetting/segmenter"
- "golang.org/x/image/math/fixed"
- )
- type lineInfo struct {
- xOff fixed.Int26_6
- yOff int
- width fixed.Int26_6
- ascent, descent fixed.Int26_6
- glyphs int
- }
- type glyphIndex struct {
- // glyphs holds the glyphs processed.
- glyphs []text.Glyph
- // positions contain all possible caret positions, sorted by rune index.
- positions []combinedPos
- // lines contains metadata about the size and position of each line of
- // text.
- lines []lineInfo
- // currentLineMin and currentLineMax track the dimensions of the line
- // that is being indexed.
- currentLineMin, currentLineMax fixed.Int26_6
- // currentLineGlyphs tracks how many glyphs are contained within the
- // line that is being indexed.
- currentLineGlyphs int
- // pos tracks attributes of the next valid cursor position within the indexed
- // text.
- pos combinedPos
- // prog tracks the current glyph text progression to detect bidi changes.
- prog text.Flags
- // clusterAdvance accumulates the advances of glyphs in a glyph cluster.
- clusterAdvance fixed.Int26_6
- // truncated indicates that the text was truncated by the shaper.
- truncated bool
- // midCluster tracks whether the next glyph processed is not the first glyph in a
- // cluster.
- midCluster bool
- }
- // reset prepares the index for reuse.
- func (g *glyphIndex) reset() {
- g.glyphs = g.glyphs[:0]
- g.positions = g.positions[:0]
- g.lines = g.lines[:0]
- g.currentLineMin = 0
- g.currentLineMax = 0
- g.currentLineGlyphs = 0
- g.pos = combinedPos{}
- g.prog = 0
- g.clusterAdvance = 0
- g.truncated = false
- g.midCluster = false
- }
- // screenPos represents a character position in text line and column numbers,
- // not pixels.
- type screenPos struct {
- // col is the column, measured in runes.
- // FIXME: we only ever use col for start or end of lines.
- // We don't need accurate accounting, so can we get rid of it?
- col int
- line int
- }
- // combinedPos is a point in the editor.
- type combinedPos struct {
- // runes is the offset in runes.
- runes int
- lineCol screenPos
- // Pixel coordinates
- x fixed.Int26_6
- y int
- ascent, descent fixed.Int26_6
- // runIndex tracks which run this position is within, counted each time
- // the index processes an end of run marker.
- runIndex int
- // towardOrigin tracks whether this glyph's run is progressing toward the
- // origin or away from it.
- towardOrigin bool
- }
- // incrementPosition returns the next position after pos (if any). Pos _must_ be
- // an unmodified position acquired from one of the closest* methods. If eof is
- // true, there was no next position.
- func (g *glyphIndex) incrementPosition(pos combinedPos) (next combinedPos, eof bool) {
- candidate, index := g.closestToRune(pos.runes)
- for candidate != pos && index+1 < len(g.positions) {
- index++
- candidate = g.positions[index]
- }
- if index+1 < len(g.positions) {
- return g.positions[index+1], false
- }
- return candidate, true
- }
- func (g *glyphIndex) insertPosition(pos combinedPos) {
- lastIdx := len(g.positions) - 1
- if lastIdx >= 0 {
- lastPos := g.positions[lastIdx]
- if lastPos.runes == pos.runes && (lastPos.y != pos.y || (lastPos.x == pos.x)) {
- // If we insert a consecutive position with the same logical position,
- // overwrite the previous position with the new one.
- g.positions[lastIdx] = pos
- return
- }
- }
- g.positions = append(g.positions, pos)
- }
- // Glyph indexes the provided glyph, generating text cursor positions for it.
- func (g *glyphIndex) Glyph(gl text.Glyph) {
- g.glyphs = append(g.glyphs, gl)
- g.currentLineGlyphs++
- if len(g.positions) == 0 {
- // First-iteration setup.
- g.currentLineMin = math.MaxInt32
- g.currentLineMax = 0
- }
- if gl.X < g.currentLineMin {
- g.currentLineMin = gl.X
- }
- if end := gl.X + gl.Advance; end > g.currentLineMax {
- g.currentLineMax = end
- }
- needsNewLine := gl.Flags&text.FlagLineBreak != 0
- needsNewRun := gl.Flags&text.FlagRunBreak != 0
- breaksParagraph := gl.Flags&text.FlagParagraphBreak != 0
- breaksCluster := gl.Flags&text.FlagClusterBreak != 0
- // We should insert new positions if the glyph we're processing terminates
- // a glyph cluster, has nonzero runes, and is not a hard newline.
- insertPositionsWithin := breaksCluster && !breaksParagraph && gl.Runes > 0
- // Get the text progression/direction right.
- g.prog = gl.Flags & text.FlagTowardOrigin
- g.pos.towardOrigin = g.prog == text.FlagTowardOrigin
- if !g.midCluster {
- // Create the text position prior to the glyph.
- g.pos.x = gl.X
- g.pos.y = int(gl.Y)
- g.pos.ascent = gl.Ascent
- g.pos.descent = gl.Descent
- if g.pos.towardOrigin {
- g.pos.x += gl.Advance
- }
- g.insertPosition(g.pos)
- }
- g.midCluster = !breaksCluster
- if breaksParagraph {
- // Paragraph breaking clusters shouldn't have positions generated for both
- // sides of them. They're always zero-width, so doing so would
- // create two visually identical cursor positions. Just reset
- // cluster state, increment by their runes, and move on to the
- // next glyph.
- g.clusterAdvance = 0
- g.pos.runes += int(gl.Runes)
- }
- // Always track the cumulative advance added by the glyph, even if it
- // doesn't terminate a cluster itself.
- g.clusterAdvance += gl.Advance
- if insertPositionsWithin {
- // Construct the text positions _within_ gl.
- g.pos.y = int(gl.Y)
- g.pos.ascent = gl.Ascent
- g.pos.descent = gl.Descent
- width := g.clusterAdvance
- positionCount := int(gl.Runes)
- runesPerPosition := 1
- if gl.Flags&text.FlagTruncator != 0 {
- // Treat the truncator as a single unit that is either selected or not.
- positionCount = 1
- runesPerPosition = int(gl.Runes)
- g.truncated = true
- }
- perRune := width / fixed.Int26_6(positionCount)
- adjust := fixed.Int26_6(0)
- if g.pos.towardOrigin {
- // If RTL, subtract increments from the width of the cluster
- // instead of adding.
- adjust = width
- perRune = -perRune
- }
- for i := 1; i <= positionCount; i++ {
- g.pos.x = gl.X + adjust + perRune*fixed.Int26_6(i)
- g.pos.runes += runesPerPosition
- g.pos.lineCol.col += runesPerPosition
- g.insertPosition(g.pos)
- }
- g.clusterAdvance = 0
- }
- if needsNewRun {
- g.pos.runIndex++
- }
- if needsNewLine {
- g.lines = append(g.lines, lineInfo{
- xOff: g.currentLineMin,
- yOff: int(gl.Y),
- width: g.currentLineMax - g.currentLineMin,
- ascent: g.positions[len(g.positions)-1].ascent,
- descent: g.positions[len(g.positions)-1].descent,
- glyphs: g.currentLineGlyphs,
- })
- g.pos.lineCol.line++
- g.pos.lineCol.col = 0
- g.pos.runIndex = 0
- g.currentLineMin = math.MaxInt32
- g.currentLineMax = 0
- g.currentLineGlyphs = 0
- }
- }
- func (g *glyphIndex) closestToRune(runeIdx int) (combinedPos, int) {
- if len(g.positions) == 0 {
- return combinedPos{}, 0
- }
- i := sort.Search(len(g.positions), func(i int) bool {
- pos := g.positions[i]
- return pos.runes >= runeIdx
- })
- if i > 0 {
- i--
- }
- closest := g.positions[i]
- closestI := i
- for ; i < len(g.positions); i++ {
- if g.positions[i].runes == runeIdx {
- return g.positions[i], i
- }
- }
- return closest, closestI
- }
- func (g *glyphIndex) closestToLineCol(lineCol screenPos) combinedPos {
- if len(g.positions) == 0 {
- return combinedPos{}
- }
- i := sort.Search(len(g.positions), func(i int) bool {
- pos := g.positions[i]
- return pos.lineCol.line > lineCol.line || (pos.lineCol.line == lineCol.line && pos.lineCol.col >= lineCol.col)
- })
- if i > 0 {
- i--
- }
- prior := g.positions[i]
- if i+1 >= len(g.positions) {
- return prior
- }
- next := g.positions[i+1]
- if next.lineCol != lineCol {
- return prior
- }
- return next
- }
- func dist(a, b fixed.Int26_6) fixed.Int26_6 {
- if a > b {
- return a - b
- }
- return b - a
- }
- func (g *glyphIndex) closestToXY(x fixed.Int26_6, y int) combinedPos {
- if len(g.positions) == 0 {
- return combinedPos{}
- }
- i := sort.Search(len(g.positions), func(i int) bool {
- pos := g.positions[i]
- return pos.y+pos.descent.Round() >= y
- })
- // If no position was greater than the provided Y, the text is too
- // short. Return either the last position or (if there are no
- // positions) the zero position.
- if i == len(g.positions) {
- return g.positions[i-1]
- }
- first := g.positions[i]
- // Find the best X coordinate.
- closest := i
- closestDist := dist(first.x, x)
- line := first.lineCol.line
- // NOTE(whereswaldon): there isn't a simple way to accelerate this. Bidi text means that the x coordinates
- // for positions have no fixed relationship. In the future, we can consider sorting the positions
- // on a line by their x coordinate and caching that. It'll be a one-time O(nlogn) per line, but
- // subsequent uses of this function for that line become O(logn). Right now it's always O(n).
- for i := i + 1; i < len(g.positions) && g.positions[i].lineCol.line == line; i++ {
- candidate := g.positions[i]
- distance := dist(candidate.x, x)
- // If we are *really* close to the current position candidate, just choose it.
- if distance.Round() == 0 {
- return g.positions[i]
- }
- if distance < closestDist {
- closestDist = distance
- closest = i
- }
- }
- return g.positions[closest]
- }
- // makeRegion creates a text-aligned rectangle from start to end. The vertical
- // dimensions of the rectangle are derived from the provided line's ascent and
- // descent, and the y offset of the line's baseline is provided as y.
- func makeRegion(line lineInfo, y int, start, end fixed.Int26_6) Region {
- if start > end {
- start, end = end, start
- }
- dotStart := image.Pt(start.Round(), y)
- dotEnd := image.Pt(end.Round(), y)
- return Region{
- Bounds: image.Rectangle{
- Min: dotStart.Sub(image.Point{Y: line.ascent.Ceil()}),
- Max: dotEnd.Add(image.Point{Y: line.descent.Floor()}),
- },
- Baseline: line.descent.Floor(),
- }
- }
- // Region describes the position and baseline of an area of interest within
- // shaped text.
- type Region struct {
- // Bounds is the coordinates of the bounding box relative to the containing
- // widget.
- Bounds image.Rectangle
- // Baseline is the quantity of vertical pixels between the baseline and
- // the bottom of bounds.
- Baseline int
- }
- // locate returns highlight regions covering the glyphs that represent the runes in
- // [startRune,endRune). If the rects parameter is non-nil, locate will use it to
- // return results instead of allocating, provided that there is enough capacity.
- // The returned regions have their Bounds specified relative to the provided
- // viewport.
- func (g *glyphIndex) locate(viewport image.Rectangle, startRune, endRune int, rects []Region) []Region {
- if startRune > endRune {
- startRune, endRune = endRune, startRune
- }
- rects = rects[:0]
- caretStart, _ := g.closestToRune(startRune)
- caretEnd, _ := g.closestToRune(endRune)
- for lineIdx := caretStart.lineCol.line; lineIdx < len(g.lines); lineIdx++ {
- if lineIdx > caretEnd.lineCol.line {
- break
- }
- pos := g.closestToLineCol(screenPos{line: lineIdx})
- if int(pos.y)+pos.descent.Ceil() < viewport.Min.Y {
- continue
- }
- if int(pos.y)-pos.ascent.Ceil() > viewport.Max.Y {
- break
- }
- line := g.lines[lineIdx]
- if lineIdx > caretStart.lineCol.line && lineIdx < caretEnd.lineCol.line {
- startX := line.xOff
- endX := startX + line.width
- // The entire line is selected.
- rects = append(rects, makeRegion(line, pos.y, startX, endX))
- continue
- }
- selectionStart := caretStart
- selectionEnd := caretEnd
- if lineIdx != caretStart.lineCol.line {
- // This line does not contain the beginning of the selection.
- selectionStart = g.closestToLineCol(screenPos{line: lineIdx})
- }
- if lineIdx != caretEnd.lineCol.line {
- // This line does not contain the end of the selection.
- selectionEnd = g.closestToLineCol(screenPos{line: lineIdx, col: math.MaxInt})
- }
- var (
- startX, endX fixed.Int26_6
- eof bool
- )
- lineLoop:
- for !eof {
- startX = selectionStart.x
- if selectionStart.runIndex == selectionEnd.runIndex {
- // Commit selection.
- endX = selectionEnd.x
- rects = append(rects, makeRegion(line, pos.y, startX, endX))
- break
- } else {
- currentDirection := selectionStart.towardOrigin
- previous := selectionStart
- runLoop:
- for !eof {
- // Increment the start position until the next logical run.
- for startRun := selectionStart.runIndex; selectionStart.runIndex == startRun; {
- previous = selectionStart
- selectionStart, eof = g.incrementPosition(selectionStart)
- if eof {
- endX = selectionStart.x
- rects = append(rects, makeRegion(line, pos.y, startX, endX))
- break runLoop
- }
- }
- if selectionStart.towardOrigin != currentDirection {
- endX = previous.x
- rects = append(rects, makeRegion(line, pos.y, startX, endX))
- break
- }
- if selectionStart.runIndex == selectionEnd.runIndex {
- // Commit selection.
- endX = selectionEnd.x
- rects = append(rects, makeRegion(line, pos.y, startX, endX))
- break lineLoop
- }
- }
- }
- }
- }
- for i := range rects {
- rects[i].Bounds = rects[i].Bounds.Sub(viewport.Min)
- }
- return rects
- }
- // graphemeReader segments paragraphs of text into grapheme clusters.
- type graphemeReader struct {
- segmenter.Segmenter
- graphemes []int
- paragraph []rune
- source io.ReaderAt
- cursor int64
- reader *bufio.Reader
- runeOffset int
- }
- // SetSource configures the reader to pull from source.
- func (p *graphemeReader) SetSource(source io.ReaderAt) {
- p.source = source
- p.cursor = 0
- p.reader = bufio.NewReader(p)
- p.runeOffset = 0
- }
- // Read exists to satisfy io.Reader. It should not be directly invoked.
- func (p *graphemeReader) Read(b []byte) (int, error) {
- n, err := p.source.ReadAt(b, p.cursor)
- p.cursor += int64(n)
- return n, err
- }
- // next decodes one paragraph of rune data.
- func (p *graphemeReader) next() ([]rune, bool) {
- p.paragraph = p.paragraph[:0]
- var err error
- var r rune
- for err == nil {
- r, _, err = p.reader.ReadRune()
- if err != nil {
- break
- }
- p.paragraph = append(p.paragraph, r)
- if r == '\n' {
- break
- }
- }
- return p.paragraph, err == nil
- }
- // Graphemes will return the next paragraph's grapheme cluster boundaries,
- // if any. If it returns an empty slice, there is no more data (all paragraphs
- // have been segmented).
- func (p *graphemeReader) Graphemes() []int {
- var more bool
- p.graphemes = p.graphemes[:0]
- p.paragraph, more = p.next()
- if len(p.paragraph) == 0 && !more {
- return nil
- }
- p.Segmenter.Init(p.paragraph)
- iter := p.Segmenter.GraphemeIterator()
- if iter.Next() {
- graph := iter.Grapheme()
- p.graphemes = append(p.graphemes,
- p.runeOffset+graph.Offset,
- p.runeOffset+graph.Offset+len(graph.Text),
- )
- }
- for iter.Next() {
- graph := iter.Grapheme()
- p.graphemes = append(p.graphemes, p.runeOffset+graph.Offset+len(graph.Text))
- }
- p.runeOffset += len(p.paragraph)
- return p.graphemes
- }
|