textgrid.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. package widget
  2. import (
  3. "image/color"
  4. "math"
  5. "strconv"
  6. "strings"
  7. "fyne.io/fyne/v2/internal/cache"
  8. "fyne.io/fyne/v2/internal/painter"
  9. "fyne.io/fyne/v2"
  10. "fyne.io/fyne/v2/canvas"
  11. "fyne.io/fyne/v2/theme"
  12. )
  13. const (
  14. textAreaSpaceSymbol = '·'
  15. textAreaTabSymbol = '→'
  16. textAreaNewLineSymbol = '↵'
  17. )
  18. var (
  19. // TextGridStyleDefault is a default style for test grid cells
  20. TextGridStyleDefault TextGridStyle
  21. // TextGridStyleWhitespace is the style used for whitespace characters, if enabled
  22. TextGridStyleWhitespace TextGridStyle
  23. )
  24. // TextGridCell represents a single cell in a text grid.
  25. // It has a rune for the text content and a style associated with it.
  26. type TextGridCell struct {
  27. Rune rune
  28. Style TextGridStyle
  29. }
  30. // TextGridRow represents a row of cells cell in a text grid.
  31. // It contains the cells for the row and an optional style.
  32. type TextGridRow struct {
  33. Cells []TextGridCell
  34. Style TextGridStyle
  35. }
  36. // TextGridStyle defines a style that can be applied to a TextGrid cell.
  37. type TextGridStyle interface {
  38. TextColor() color.Color
  39. BackgroundColor() color.Color
  40. }
  41. // CustomTextGridStyle is a utility type for those not wanting to define their own style types.
  42. type CustomTextGridStyle struct {
  43. FGColor, BGColor color.Color
  44. }
  45. // TextColor is the color a cell should use for the text.
  46. func (c *CustomTextGridStyle) TextColor() color.Color {
  47. return c.FGColor
  48. }
  49. // BackgroundColor is the color a cell should use for the background.
  50. func (c *CustomTextGridStyle) BackgroundColor() color.Color {
  51. return c.BGColor
  52. }
  53. // TextGrid is a monospaced grid of characters.
  54. // This is designed to be used by a text editor, code preview or terminal emulator.
  55. type TextGrid struct {
  56. BaseWidget
  57. Rows []TextGridRow
  58. ShowLineNumbers bool
  59. ShowWhitespace bool
  60. TabWidth int // If set to 0 the fyne.DefaultTabWidth is used
  61. }
  62. // MinSize returns the smallest size this widget can shrink to
  63. func (t *TextGrid) MinSize() fyne.Size {
  64. t.ExtendBaseWidget(t)
  65. return t.BaseWidget.MinSize()
  66. }
  67. // Resize is called when this widget changes size. We should make sure that we refresh cells.
  68. func (t *TextGrid) Resize(size fyne.Size) {
  69. t.BaseWidget.Resize(size)
  70. t.Refresh()
  71. }
  72. // SetText updates the buffer of this textgrid to contain the specified text.
  73. // New lines and columns will be added as required. Lines are separated by '\n'.
  74. // The grid will use default text style and any previous content and style will be removed.
  75. // Tab characters are padded with spaces to the next tab stop.
  76. func (t *TextGrid) SetText(text string) {
  77. lines := strings.Split(text, "\n")
  78. rows := make([]TextGridRow, len(lines))
  79. for i, line := range lines {
  80. cells := make([]TextGridCell, 0, len(line))
  81. for _, r := range line {
  82. cells = append(cells, TextGridCell{Rune: r})
  83. if r == '\t' {
  84. col := len(cells)
  85. next := nextTab(col-1, t.tabWidth())
  86. for i := col; i < next; i++ {
  87. cells = append(cells, TextGridCell{Rune: ' '})
  88. }
  89. }
  90. }
  91. rows[i] = TextGridRow{Cells: cells}
  92. }
  93. t.Rows = rows
  94. t.Refresh()
  95. }
  96. // Text returns the contents of the buffer as a single string (with no style information).
  97. // It reconstructs the lines by joining with a `\n` character.
  98. // Tab characters have padded spaces removed.
  99. func (t *TextGrid) Text() string {
  100. count := len(t.Rows) - 1 // newlines
  101. for _, row := range t.Rows {
  102. count += len(row.Cells)
  103. }
  104. if count <= 0 {
  105. return ""
  106. }
  107. runes := make([]rune, 0, count)
  108. for i, row := range t.Rows {
  109. next := 0
  110. for col, cell := range row.Cells {
  111. if col < next {
  112. continue
  113. }
  114. runes = append(runes, cell.Rune)
  115. if cell.Rune == '\t' {
  116. next = nextTab(col, t.tabWidth())
  117. }
  118. }
  119. if i < len(t.Rows)-1 {
  120. runes = append(runes, '\n')
  121. }
  122. }
  123. return string(runes)
  124. }
  125. // Row returns a copy of the content in a specified row as a TextGridRow.
  126. // If the index is out of bounds it returns an empty row object.
  127. func (t *TextGrid) Row(row int) TextGridRow {
  128. if row < 0 || row >= len(t.Rows) {
  129. return TextGridRow{}
  130. }
  131. return t.Rows[row]
  132. }
  133. // RowText returns a string representation of the content at the row specified.
  134. // If the index is out of bounds it returns an empty string.
  135. func (t *TextGrid) RowText(row int) string {
  136. rowData := t.Row(row)
  137. count := len(rowData.Cells)
  138. if count <= 0 {
  139. return ""
  140. }
  141. runes := make([]rune, 0, count)
  142. next := 0
  143. for col, cell := range rowData.Cells {
  144. if col < next {
  145. continue
  146. }
  147. runes = append(runes, cell.Rune)
  148. if cell.Rune == '\t' {
  149. next = nextTab(col, t.tabWidth())
  150. }
  151. }
  152. return string(runes)
  153. }
  154. // SetRow updates the specified row of the grid's contents using the specified content and style and then refreshes.
  155. // If the row is beyond the end of the current buffer it will be expanded.
  156. // Tab characters are not padded with spaces.
  157. func (t *TextGrid) SetRow(row int, content TextGridRow) {
  158. if row < 0 {
  159. return
  160. }
  161. for len(t.Rows) <= row {
  162. t.Rows = append(t.Rows, TextGridRow{})
  163. }
  164. t.Rows[row] = content
  165. for col := 0; col > len(content.Cells); col++ {
  166. t.refreshCell(row, col)
  167. }
  168. }
  169. // SetRowStyle sets a grid style to all the cells cell at the specified row.
  170. // Any cells in this row with their own style will override this value when displayed.
  171. func (t *TextGrid) SetRowStyle(row int, style TextGridStyle) {
  172. if row < 0 {
  173. return
  174. }
  175. for len(t.Rows) <= row {
  176. t.Rows = append(t.Rows, TextGridRow{})
  177. }
  178. t.Rows[row].Style = style
  179. }
  180. // SetCell sets a grid data to the cell at named row and column.
  181. func (t *TextGrid) SetCell(row, col int, cell TextGridCell) {
  182. if row < 0 || col < 0 {
  183. return
  184. }
  185. t.ensureCells(row, col)
  186. t.Rows[row].Cells[col] = cell
  187. t.refreshCell(row, col)
  188. }
  189. // SetRune sets a character to the cell at named row and column.
  190. func (t *TextGrid) SetRune(row, col int, r rune) {
  191. if row < 0 || col < 0 {
  192. return
  193. }
  194. t.ensureCells(row, col)
  195. t.Rows[row].Cells[col].Rune = r
  196. t.refreshCell(row, col)
  197. }
  198. // SetStyle sets a grid style to the cell at named row and column.
  199. func (t *TextGrid) SetStyle(row, col int, style TextGridStyle) {
  200. if row < 0 || col < 0 {
  201. return
  202. }
  203. t.ensureCells(row, col)
  204. t.Rows[row].Cells[col].Style = style
  205. t.refreshCell(row, col)
  206. }
  207. // SetStyleRange sets a grid style to all the cells between the start row and column through to the end row and column.
  208. func (t *TextGrid) SetStyleRange(startRow, startCol, endRow, endCol int, style TextGridStyle) {
  209. if startRow >= len(t.Rows) || endRow < 0 {
  210. return
  211. }
  212. if startRow < 0 {
  213. startRow = 0
  214. startCol = 0
  215. }
  216. if endRow >= len(t.Rows) {
  217. endRow = len(t.Rows) - 1
  218. endCol = len(t.Rows[endRow].Cells) - 1
  219. }
  220. if startRow == endRow {
  221. for col := startCol; col <= endCol; col++ {
  222. t.SetStyle(startRow, col, style)
  223. }
  224. return
  225. }
  226. // first row
  227. for col := startCol; col < len(t.Rows[startRow].Cells); col++ {
  228. t.SetStyle(startRow, col, style)
  229. }
  230. // possible middle rows
  231. for rowNum := startRow + 1; rowNum < endRow; rowNum++ {
  232. for col := 0; col < len(t.Rows[rowNum].Cells); col++ {
  233. t.SetStyle(rowNum, col, style)
  234. }
  235. }
  236. // last row
  237. for col := 0; col <= endCol; col++ {
  238. t.SetStyle(endRow, col, style)
  239. }
  240. }
  241. // CreateRenderer is a private method to Fyne which links this widget to it's renderer
  242. func (t *TextGrid) CreateRenderer() fyne.WidgetRenderer {
  243. t.ExtendBaseWidget(t)
  244. render := &textGridRenderer{text: t}
  245. render.updateCellSize()
  246. TextGridStyleDefault = &CustomTextGridStyle{}
  247. TextGridStyleWhitespace = &CustomTextGridStyle{FGColor: theme.DisabledColor()}
  248. return render
  249. }
  250. func (t *TextGrid) ensureCells(row, col int) {
  251. for len(t.Rows) <= row {
  252. t.Rows = append(t.Rows, TextGridRow{})
  253. }
  254. data := t.Rows[row]
  255. for len(data.Cells) <= col {
  256. data.Cells = append(data.Cells, TextGridCell{})
  257. t.Rows[row] = data
  258. }
  259. }
  260. func (t *TextGrid) refreshCell(row, col int) {
  261. r := cache.Renderer(t).(*textGridRenderer)
  262. r.refreshCell(row, col)
  263. }
  264. // NewTextGrid creates a new empty TextGrid widget.
  265. func NewTextGrid() *TextGrid {
  266. grid := &TextGrid{}
  267. grid.ExtendBaseWidget(grid)
  268. return grid
  269. }
  270. // NewTextGridFromString creates a new TextGrid widget with the specified string content.
  271. func NewTextGridFromString(content string) *TextGrid {
  272. grid := NewTextGrid()
  273. grid.SetText(content)
  274. return grid
  275. }
  276. // nextTab finds the column of the next tab stop for the given column
  277. func nextTab(column int, tabWidth int) int {
  278. tabStop, _ := math.Modf(float64(column+tabWidth) / float64(tabWidth))
  279. return tabWidth * int(tabStop)
  280. }
  281. type textGridRenderer struct {
  282. text *TextGrid
  283. cols, rows int
  284. cellSize fyne.Size
  285. objects []fyne.CanvasObject
  286. current fyne.Canvas
  287. }
  288. func (t *textGridRenderer) appendTextCell(str rune) {
  289. text := canvas.NewText(string(str), theme.ForegroundColor())
  290. text.TextStyle.Monospace = true
  291. bg := canvas.NewRectangle(color.Transparent)
  292. t.objects = append(t.objects, bg, text)
  293. }
  294. func (t *textGridRenderer) refreshCell(row, col int) {
  295. pos := row*t.cols + col
  296. if pos*2+1 >= len(t.objects) {
  297. return
  298. }
  299. cell := t.text.Rows[row].Cells[col]
  300. t.setCellRune(cell.Rune, pos, cell.Style, t.text.Rows[row].Style)
  301. }
  302. func (t *textGridRenderer) setCellRune(str rune, pos int, style, rowStyle TextGridStyle) {
  303. if str == 0 {
  304. str = ' '
  305. }
  306. text := t.objects[pos*2+1].(*canvas.Text)
  307. text.TextSize = theme.TextSize()
  308. fg := theme.ForegroundColor()
  309. if style != nil && style.TextColor() != nil {
  310. fg = style.TextColor()
  311. } else if rowStyle != nil && rowStyle.TextColor() != nil {
  312. fg = rowStyle.TextColor()
  313. }
  314. newStr := string(str)
  315. if text.Text != newStr || text.Color != fg {
  316. text.Text = newStr
  317. text.Color = fg
  318. t.refresh(text)
  319. }
  320. rect := t.objects[pos*2].(*canvas.Rectangle)
  321. bg := color.Color(color.Transparent)
  322. if style != nil && style.BackgroundColor() != nil {
  323. bg = style.BackgroundColor()
  324. } else if rowStyle != nil && rowStyle.BackgroundColor() != nil {
  325. bg = rowStyle.BackgroundColor()
  326. }
  327. if rect.FillColor != bg {
  328. rect.FillColor = bg
  329. t.refresh(rect)
  330. }
  331. }
  332. func (t *textGridRenderer) addCellsIfRequired() {
  333. cellCount := t.cols * t.rows
  334. if len(t.objects) == cellCount*2 {
  335. return
  336. }
  337. for i := len(t.objects); i < cellCount*2; i += 2 {
  338. t.appendTextCell(' ')
  339. }
  340. }
  341. func (t *textGridRenderer) refreshGrid() {
  342. line := 1
  343. x := 0
  344. for rowIndex, row := range t.text.Rows {
  345. rowStyle := row.Style
  346. i := 0
  347. if t.text.ShowLineNumbers {
  348. lineStr := []rune(strconv.Itoa(line))
  349. pad := t.lineNumberWidth() - len(lineStr)
  350. for ; i < pad; i++ {
  351. t.setCellRune(' ', x, TextGridStyleWhitespace, rowStyle) // padding space
  352. x++
  353. }
  354. for c := 0; c < len(lineStr); c++ {
  355. t.setCellRune(lineStr[c], x, TextGridStyleDefault, rowStyle) // line numbers
  356. i++
  357. x++
  358. }
  359. t.setCellRune('|', x, TextGridStyleWhitespace, rowStyle) // last space
  360. i++
  361. x++
  362. }
  363. for _, r := range row.Cells {
  364. if i >= t.cols { // would be an overflow - bad
  365. continue
  366. }
  367. if t.text.ShowWhitespace && (r.Rune == ' ' || r.Rune == '\t') {
  368. sym := textAreaSpaceSymbol
  369. if r.Rune == '\t' {
  370. sym = textAreaTabSymbol
  371. }
  372. if r.Style != nil && r.Style.BackgroundColor() != nil {
  373. whitespaceBG := &CustomTextGridStyle{FGColor: TextGridStyleWhitespace.TextColor(),
  374. BGColor: r.Style.BackgroundColor()}
  375. t.setCellRune(sym, x, whitespaceBG, rowStyle) // whitespace char
  376. } else {
  377. t.setCellRune(sym, x, TextGridStyleWhitespace, rowStyle) // whitespace char
  378. }
  379. } else {
  380. t.setCellRune(r.Rune, x, r.Style, rowStyle) // regular char
  381. }
  382. i++
  383. x++
  384. }
  385. if t.text.ShowWhitespace && i < t.cols && rowIndex < len(t.text.Rows)-1 {
  386. t.setCellRune(textAreaNewLineSymbol, x, TextGridStyleWhitespace, rowStyle) // newline
  387. i++
  388. x++
  389. }
  390. for ; i < t.cols; i++ {
  391. t.setCellRune(' ', x, TextGridStyleDefault, rowStyle) // blanks
  392. x++
  393. }
  394. line++
  395. }
  396. for ; x < len(t.objects)/2; x++ {
  397. t.setCellRune(' ', x, TextGridStyleDefault, nil) // trailing cells and blank lines
  398. }
  399. }
  400. // tabWidth either returns the set tab width or if not set the returns the DefaultTabWidth
  401. func (t *TextGrid) tabWidth() int {
  402. if t.TabWidth == 0 {
  403. return painter.DefaultTabWidth
  404. }
  405. return t.TabWidth
  406. }
  407. func (t *textGridRenderer) lineNumberWidth() int {
  408. return len(strconv.Itoa(t.rows + 1))
  409. }
  410. func (t *textGridRenderer) updateGridSize(size fyne.Size) {
  411. bufRows := len(t.text.Rows)
  412. bufCols := 0
  413. for _, row := range t.text.Rows {
  414. bufCols = int(math.Max(float64(bufCols), float64(len(row.Cells))))
  415. }
  416. sizeCols := math.Floor(float64(size.Width) / float64(t.cellSize.Width))
  417. sizeRows := math.Floor(float64(size.Height) / float64(t.cellSize.Height))
  418. if t.text.ShowWhitespace {
  419. bufCols++
  420. }
  421. if t.text.ShowLineNumbers {
  422. bufCols += t.lineNumberWidth()
  423. }
  424. t.cols = int(math.Max(sizeCols, float64(bufCols)))
  425. t.rows = int(math.Max(sizeRows, float64(bufRows)))
  426. t.addCellsIfRequired()
  427. }
  428. func (t *textGridRenderer) Layout(size fyne.Size) {
  429. t.updateGridSize(size)
  430. i := 0
  431. cellPos := fyne.NewPos(0, 0)
  432. for y := 0; y < t.rows; y++ {
  433. for x := 0; x < t.cols; x++ {
  434. t.objects[i*2+1].Move(cellPos)
  435. t.objects[i*2].Resize(t.cellSize)
  436. t.objects[i*2].Move(cellPos)
  437. cellPos.X += t.cellSize.Width
  438. i++
  439. }
  440. cellPos.X = 0
  441. cellPos.Y += t.cellSize.Height
  442. }
  443. }
  444. func (t *textGridRenderer) MinSize() fyne.Size {
  445. longestRow := float32(0)
  446. for _, row := range t.text.Rows {
  447. longestRow = fyne.Max(longestRow, float32(len(row.Cells)))
  448. }
  449. return fyne.NewSize(t.cellSize.Width*longestRow,
  450. t.cellSize.Height*float32(len(t.text.Rows)))
  451. }
  452. func (t *textGridRenderer) Refresh() {
  453. // we may be on a new canvas, so just update it to be sure
  454. if fyne.CurrentApp() != nil && fyne.CurrentApp().Driver() != nil {
  455. t.current = fyne.CurrentApp().Driver().CanvasForObject(t.text)
  456. }
  457. // theme could change text size
  458. t.updateCellSize()
  459. TextGridStyleWhitespace = &CustomTextGridStyle{FGColor: theme.DisabledColor()}
  460. t.updateGridSize(t.text.size)
  461. t.refreshGrid()
  462. }
  463. func (t *textGridRenderer) ApplyTheme() {
  464. }
  465. func (t *textGridRenderer) Objects() []fyne.CanvasObject {
  466. return t.objects
  467. }
  468. func (t *textGridRenderer) Destroy() {
  469. }
  470. func (t *textGridRenderer) refresh(obj fyne.CanvasObject) {
  471. if t.current == nil {
  472. if fyne.CurrentApp() != nil && fyne.CurrentApp().Driver() != nil {
  473. // cache canvas for this widget, so we don't look it up many times for every cell/row refresh!
  474. t.current = fyne.CurrentApp().Driver().CanvasForObject(t.text)
  475. }
  476. if t.current == nil {
  477. return // not yet set up perhaps?
  478. }
  479. }
  480. t.current.Refresh(obj)
  481. }
  482. func (t *textGridRenderer) updateCellSize() {
  483. size := fyne.MeasureText("M", theme.TextSize(), fyne.TextStyle{Monospace: true})
  484. // round it for seamless background
  485. size.Width = float32(math.Round(float64((size.Width))))
  486. size.Height = float32(math.Round(float64((size.Height))))
  487. t.cellSize = size
  488. }