selectable.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. package widget
  2. import (
  3. "image"
  4. "io"
  5. "math"
  6. "strings"
  7. "gioui.org/font"
  8. "gioui.org/gesture"
  9. "gioui.org/io/clipboard"
  10. "gioui.org/io/event"
  11. "gioui.org/io/key"
  12. "gioui.org/io/pointer"
  13. "gioui.org/io/system"
  14. "gioui.org/layout"
  15. "gioui.org/op"
  16. "gioui.org/op/clip"
  17. "gioui.org/text"
  18. "gioui.org/unit"
  19. )
  20. // stringSource is an immutable textSource with a fixed string
  21. // value.
  22. type stringSource struct {
  23. reader *strings.Reader
  24. }
  25. var _ textSource = stringSource{}
  26. func newStringSource(str string) stringSource {
  27. return stringSource{
  28. reader: strings.NewReader(str),
  29. }
  30. }
  31. func (s stringSource) Changed() bool {
  32. return false
  33. }
  34. func (s stringSource) Size() int64 {
  35. return s.reader.Size()
  36. }
  37. func (s stringSource) ReadAt(b []byte, offset int64) (int, error) {
  38. return s.reader.ReadAt(b, offset)
  39. }
  40. // ReplaceRunes is unimplemented, as a stringSource is immutable.
  41. func (s stringSource) ReplaceRunes(byteOffset, runeCount int64, str string) {
  42. }
  43. // Selectable displays selectable text.
  44. type Selectable struct {
  45. // Alignment controls the alignment of the text.
  46. Alignment text.Alignment
  47. // MaxLines is the maximum number of lines of text to be displayed.
  48. MaxLines int
  49. // Truncator is the symbol to use at the end of the final line of text
  50. // if text was cut off. Defaults to "…" if left empty.
  51. Truncator string
  52. // WrapPolicy configures how displayed text will be broken into lines.
  53. WrapPolicy text.WrapPolicy
  54. // LineHeight controls the distance between the baselines of lines of text.
  55. // If zero, a sensible default will be used.
  56. LineHeight unit.Sp
  57. // LineHeightScale applies a scaling factor to the LineHeight. If zero, a
  58. // sensible default will be used.
  59. LineHeightScale float32
  60. initialized bool
  61. source stringSource
  62. // scratch is a buffer reused to efficiently read text out of the
  63. // textView.
  64. scratch []byte
  65. lastValue string
  66. text textView
  67. focused bool
  68. dragging bool
  69. dragger gesture.Drag
  70. clicker gesture.Click
  71. }
  72. // initialize must be called at the beginning of any exported method that
  73. // manipulates text state. It ensures that the underlying text is safe to
  74. // access.
  75. func (l *Selectable) initialize() {
  76. if !l.initialized {
  77. l.source = newStringSource("")
  78. l.text.SetSource(l.source)
  79. l.initialized = true
  80. }
  81. }
  82. // Focused returns whether the label is focused or not.
  83. func (l *Selectable) Focused() bool {
  84. return l.focused
  85. }
  86. // paintSelection paints the contrasting background for selected text.
  87. func (l *Selectable) paintSelection(gtx layout.Context, material op.CallOp) {
  88. l.initialize()
  89. if !l.focused {
  90. return
  91. }
  92. l.text.PaintSelection(gtx, material)
  93. }
  94. // paintText paints the text glyphs with the provided material.
  95. func (l *Selectable) paintText(gtx layout.Context, material op.CallOp) {
  96. l.initialize()
  97. l.text.PaintText(gtx, material)
  98. }
  99. // SelectionLen returns the length of the selection, in runes; it is
  100. // equivalent to utf8.RuneCountInString(e.SelectedText()).
  101. func (l *Selectable) SelectionLen() int {
  102. l.initialize()
  103. return l.text.SelectionLen()
  104. }
  105. // Selection returns the start and end of the selection, as rune offsets.
  106. // start can be > end.
  107. func (l *Selectable) Selection() (start, end int) {
  108. l.initialize()
  109. return l.text.Selection()
  110. }
  111. // SetCaret moves the caret to start, and sets the selection end to end. start
  112. // and end are in runes, and represent offsets into the editor text.
  113. func (l *Selectable) SetCaret(start, end int) {
  114. l.initialize()
  115. l.text.SetCaret(start, end)
  116. }
  117. // SelectedText returns the currently selected text (if any) from the editor.
  118. func (l *Selectable) SelectedText() string {
  119. l.initialize()
  120. l.scratch = l.text.SelectedText(l.scratch)
  121. return string(l.scratch)
  122. }
  123. // ClearSelection clears the selection, by setting the selection end equal to
  124. // the selection start.
  125. func (l *Selectable) ClearSelection() {
  126. l.initialize()
  127. l.text.ClearSelection()
  128. }
  129. // Text returns the contents of the label.
  130. func (l *Selectable) Text() string {
  131. l.initialize()
  132. l.scratch = l.text.Text(l.scratch)
  133. return string(l.scratch)
  134. }
  135. // SetText updates the text to s if it does not already contain s. Updating the
  136. // text will clear the selection unless the selectable already contains s.
  137. func (l *Selectable) SetText(s string) {
  138. l.initialize()
  139. if l.lastValue != s {
  140. l.source = newStringSource(s)
  141. l.lastValue = s
  142. l.text.SetSource(l.source)
  143. }
  144. }
  145. // Truncated returns whether the text has been truncated by the text shaper to
  146. // fit within available constraints.
  147. func (l *Selectable) Truncated() bool {
  148. return l.text.Truncated()
  149. }
  150. // Update the state of the selectable in response to input events. It returns whether the
  151. // text selection changed during event processing.
  152. func (l *Selectable) Update(gtx layout.Context) bool {
  153. l.initialize()
  154. return l.handleEvents(gtx)
  155. }
  156. // Layout clips to the dimensions of the selectable, updates the shaped text, configures input handling, and paints
  157. // the text and selection rectangles. The provided textMaterial and selectionMaterial ops are used to set the
  158. // paint material for the text and selection rectangles, respectively.
  159. func (l *Selectable) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectionMaterial op.CallOp) layout.Dimensions {
  160. l.Update(gtx)
  161. l.text.LineHeight = l.LineHeight
  162. l.text.LineHeightScale = l.LineHeightScale
  163. l.text.Alignment = l.Alignment
  164. l.text.MaxLines = l.MaxLines
  165. l.text.Truncator = l.Truncator
  166. l.text.WrapPolicy = l.WrapPolicy
  167. l.text.Layout(gtx, lt, font, size)
  168. dims := l.text.Dimensions()
  169. defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop()
  170. pointer.CursorText.Add(gtx.Ops)
  171. event.Op(gtx.Ops, l)
  172. l.clicker.Add(gtx.Ops)
  173. l.dragger.Add(gtx.Ops)
  174. l.paintSelection(gtx, selectionMaterial)
  175. l.paintText(gtx, textMaterial)
  176. return dims
  177. }
  178. func (l *Selectable) handleEvents(gtx layout.Context) (selectionChanged bool) {
  179. oldStart, oldLen := min(l.text.Selection()), l.text.SelectionLen()
  180. defer func() {
  181. if newStart, newLen := min(l.text.Selection()), l.text.SelectionLen(); oldStart != newStart || oldLen != newLen {
  182. selectionChanged = true
  183. }
  184. }()
  185. l.processPointer(gtx)
  186. l.processKey(gtx)
  187. return selectionChanged
  188. }
  189. func (e *Selectable) processPointer(gtx layout.Context) {
  190. for _, evt := range e.clickDragEvents(gtx) {
  191. switch evt := evt.(type) {
  192. case gesture.ClickEvent:
  193. switch {
  194. case evt.Kind == gesture.KindPress && evt.Source == pointer.Mouse,
  195. evt.Kind == gesture.KindClick && evt.Source != pointer.Mouse:
  196. prevCaretPos, _ := e.text.Selection()
  197. e.text.MoveCoord(image.Point{
  198. X: int(math.Round(float64(evt.Position.X))),
  199. Y: int(math.Round(float64(evt.Position.Y))),
  200. })
  201. gtx.Execute(key.FocusCmd{Tag: e})
  202. if evt.Modifiers == key.ModShift {
  203. start, end := e.text.Selection()
  204. // If they clicked closer to the end, then change the end to
  205. // where the caret used to be (effectively swapping start & end).
  206. if abs(end-start) < abs(start-prevCaretPos) {
  207. e.text.SetCaret(start, prevCaretPos)
  208. }
  209. } else {
  210. e.text.ClearSelection()
  211. }
  212. e.dragging = true
  213. // Process multi-clicks.
  214. switch {
  215. case evt.NumClicks == 2:
  216. e.text.MoveWord(-1, selectionClear)
  217. e.text.MoveWord(1, selectionExtend)
  218. e.dragging = false
  219. case evt.NumClicks >= 3:
  220. e.text.MoveLineStart(selectionClear)
  221. e.text.MoveLineEnd(selectionExtend)
  222. e.dragging = false
  223. }
  224. }
  225. case pointer.Event:
  226. release := false
  227. switch {
  228. case evt.Kind == pointer.Release && evt.Source == pointer.Mouse:
  229. release = true
  230. fallthrough
  231. case evt.Kind == pointer.Drag && evt.Source == pointer.Mouse:
  232. if e.dragging {
  233. e.text.MoveCoord(image.Point{
  234. X: int(math.Round(float64(evt.Position.X))),
  235. Y: int(math.Round(float64(evt.Position.Y))),
  236. })
  237. if release {
  238. e.dragging = false
  239. }
  240. }
  241. }
  242. }
  243. }
  244. }
  245. func (e *Selectable) clickDragEvents(gtx layout.Context) []event.Event {
  246. var combinedEvents []event.Event
  247. for {
  248. evt, ok := e.clicker.Update(gtx.Source)
  249. if !ok {
  250. break
  251. }
  252. combinedEvents = append(combinedEvents, evt)
  253. }
  254. for {
  255. evt, ok := e.dragger.Update(gtx.Metric, gtx.Source, gesture.Both)
  256. if !ok {
  257. break
  258. }
  259. combinedEvents = append(combinedEvents, evt)
  260. }
  261. return combinedEvents
  262. }
  263. func (e *Selectable) processKey(gtx layout.Context) {
  264. for {
  265. ke, ok := gtx.Event(
  266. key.FocusFilter{Target: e},
  267. key.Filter{Focus: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift},
  268. key.Filter{Focus: e, Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift},
  269. key.Filter{Focus: e, Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift},
  270. key.Filter{Focus: e, Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift},
  271. key.Filter{Focus: e, Name: key.NamePageUp, Optional: key.ModShift},
  272. key.Filter{Focus: e, Name: key.NamePageDown, Optional: key.ModShift},
  273. key.Filter{Focus: e, Name: key.NameEnd, Optional: key.ModShift},
  274. key.Filter{Focus: e, Name: key.NameHome, Optional: key.ModShift},
  275. key.Filter{Focus: e, Name: "C", Required: key.ModShortcut},
  276. key.Filter{Focus: e, Name: "X", Required: key.ModShortcut},
  277. key.Filter{Focus: e, Name: "A", Required: key.ModShortcut},
  278. )
  279. if !ok {
  280. break
  281. }
  282. switch ke := ke.(type) {
  283. case key.FocusEvent:
  284. e.focused = ke.Focus
  285. case key.Event:
  286. if !e.focused || ke.State != key.Press {
  287. break
  288. }
  289. e.command(gtx, ke)
  290. }
  291. }
  292. }
  293. func (e *Selectable) command(gtx layout.Context, k key.Event) {
  294. direction := 1
  295. if gtx.Locale.Direction.Progression() == system.TowardOrigin {
  296. direction = -1
  297. }
  298. moveByWord := k.Modifiers.Contain(key.ModShortcutAlt)
  299. selAct := selectionClear
  300. if k.Modifiers.Contain(key.ModShift) {
  301. selAct = selectionExtend
  302. }
  303. if k.Modifiers == key.ModShortcut {
  304. switch k.Name {
  305. // Copy or Cut selection -- ignored if nothing selected.
  306. case "C", "X":
  307. e.scratch = e.text.SelectedText(e.scratch)
  308. if text := string(e.scratch); text != "" {
  309. gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(strings.NewReader(text))})
  310. }
  311. // Select all
  312. case "A":
  313. e.text.SetCaret(0, e.text.Len())
  314. }
  315. return
  316. }
  317. switch k.Name {
  318. case key.NameUpArrow:
  319. e.text.MoveLines(-1, selAct)
  320. case key.NameDownArrow:
  321. e.text.MoveLines(+1, selAct)
  322. case key.NameLeftArrow:
  323. if moveByWord {
  324. e.text.MoveWord(-1*direction, selAct)
  325. } else {
  326. if selAct == selectionClear {
  327. e.text.ClearSelection()
  328. }
  329. e.text.MoveCaret(-1*direction, -1*direction*int(selAct))
  330. }
  331. case key.NameRightArrow:
  332. if moveByWord {
  333. e.text.MoveWord(1*direction, selAct)
  334. } else {
  335. if selAct == selectionClear {
  336. e.text.ClearSelection()
  337. }
  338. e.text.MoveCaret(1*direction, int(selAct)*direction)
  339. }
  340. case key.NamePageUp:
  341. e.text.MovePages(-1, selAct)
  342. case key.NamePageDown:
  343. e.text.MovePages(+1, selAct)
  344. case key.NameHome:
  345. e.text.MoveLineStart(selAct)
  346. case key.NameEnd:
  347. e.text.MoveLineEnd(selAct)
  348. }
  349. }
  350. // Regions returns visible regions covering the rune range [start,end).
  351. func (l *Selectable) Regions(start, end int, regions []Region) []Region {
  352. l.initialize()
  353. return l.text.Regions(start, end, regions)
  354. }