index.go 14 KB

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