• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

fyne-io / fyne / 18807176232

25 Oct 2025 06:34PM UTC coverage: 61.057% (-0.004%) from 61.061%
18807176232

Pull #5989

github

Jacalz
Fix TODO regarding comparable map key
Pull Request #5989: RFC: Proof of concept for upgrading Go to 1.24

155 of 188 new or added lines in 62 files covered. (82.45%)

27 existing lines in 6 files now uncovered.

25609 of 41943 relevant lines covered (61.06%)

692.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.43
/widget/textgrid.go
1
package widget
2

3
import (
4
        "image/color"
5
        "math"
6
        "strconv"
7
        "strings"
8

9
        "fyne.io/fyne/v2"
10
        "fyne.io/fyne/v2/canvas"
11
        "fyne.io/fyne/v2/internal/async"
12
        "fyne.io/fyne/v2/internal/painter"
13
        "fyne.io/fyne/v2/internal/widget"
14
        "fyne.io/fyne/v2/theme"
15
)
16

17
const (
18
        textAreaSpaceSymbol   = '·'
19
        textAreaTabSymbol     = '→'
20
        textAreaNewLineSymbol = '↵'
21
)
22

23
var (
24
        // TextGridStyleDefault is a default style for test grid cells
25
        TextGridStyleDefault TextGridStyle
26
        // TextGridStyleWhitespace is the style used for whitespace characters, if enabled
27
        TextGridStyleWhitespace TextGridStyle
28
)
29

30
// TextGridCell represents a single cell in a text grid.
31
// It has a rune for the text content and a style associated with it.
32
type TextGridCell struct {
33
        Rune  rune
34
        Style TextGridStyle
35
}
36

37
// TextGridRow represents a row of cells cell in a text grid.
38
// It contains the cells for the row and an optional style.
39
type TextGridRow struct {
40
        Cells []TextGridCell
41
        Style TextGridStyle
42
}
43

44
// TextGridStyle defines a style that can be applied to a TextGrid cell.
45
type TextGridStyle interface {
46
        Style() fyne.TextStyle
47
        TextColor() color.Color
48
        BackgroundColor() color.Color
49
}
50

51
// CustomTextGridStyle is a utility type for those not wanting to define their own style types.
52
type CustomTextGridStyle struct {
53
        // Since: 2.5
54
        TextStyle        fyne.TextStyle
55
        FGColor, BGColor color.Color
56
}
57

58
// TextColor is the color a cell should use for the text.
59
func (c *CustomTextGridStyle) TextColor() color.Color {
2,053✔
60
        return c.FGColor
2,053✔
61
}
2,053✔
62

63
// BackgroundColor is the color a cell should use for the background.
64
func (c *CustomTextGridStyle) BackgroundColor() color.Color {
2,002✔
65
        return c.BGColor
2,002✔
66
}
2,002✔
67

68
// Style is the text style a cell should use.
69
func (c *CustomTextGridStyle) Style() fyne.TextStyle {
1,988✔
70
        return c.TextStyle
1,988✔
71
}
1,988✔
72

73
// TextGrid is a monospaced grid of characters.
74
// This is designed to be used by a text editor, code preview or terminal emulator.
75
type TextGrid struct {
76
        BaseWidget
77
        Rows []TextGridRow
78

79
        scroll  *widget.Scroll
80
        content *textGridContent
81

82
        ShowLineNumbers bool
83
        ShowWhitespace  bool
84
        TabWidth        int // If set to 0 the fyne.DefaultTabWidth is used
85

86
        // Scroll can be used to turn off the scrolling of our TextGrid.
87
        //
88
        // Since: 2.6
89
        Scroll fyne.ScrollDirection
90
}
91

92
// Append will add new lines to the end of this TextGrid.
93
// The first character will be at the beginning of a new line and any newline characters will split the text further.
94
//
95
// Since: 2.6
96
func (t *TextGrid) Append(text string) {
1✔
97
        rows := t.parseRows(text)
1✔
98

1✔
99
        t.Rows = append(t.Rows, rows...)
1✔
100
        t.Refresh()
1✔
101
}
1✔
102

103
// CursorLocationForPosition returns the location where a cursor would be if it was located in the cell under the
104
// requested position. If the grid is scrolled the position will refer to the visible offset and not the distance
105
// from the top left of the overall document.
106
//
107
// Since: 2.6
108
func (t *TextGrid) CursorLocationForPosition(p fyne.Position) (row, col int) {
5✔
109
        y := p.Y
5✔
110
        x := p.X
5✔
111

5✔
112
        if t.scroll != nil && t.scroll.Visible() {
10✔
113
                y += t.scroll.Offset.Y
5✔
114
                x += t.scroll.Offset.X
5✔
115
        }
5✔
116

117
        row = int(y / t.content.cellSize.Height)
5✔
118
        col = int(x / t.content.cellSize.Width)
5✔
119
        return row, col
5✔
120
}
121

122
// ScrollToTop will scroll content to container top
123
//
124
// Since: 2.7
125
func (t *TextGrid) ScrollToTop() {
2✔
126
        t.scroll.ScrollToTop()
2✔
127
        t.Refresh()
2✔
128
}
2✔
129

130
// ScrollToBottom will scroll content to container bottom - to show latest info which end user just added
131
//
132
// Since: 2.7
133
func (t *TextGrid) ScrollToBottom() {
2✔
134
        t.scroll.ScrollToBottom()
2✔
135
        t.Refresh()
2✔
136
}
2✔
137

138
// PositionForCursorLocation returns the relative position in this TextGrid for the cell at position row, col.
139
// If the grid has been scrolled this will be taken into account so that the position compared to top left will
140
// refer to the requested location.
141
//
142
// Since: 2.6
143
func (t *TextGrid) PositionForCursorLocation(row, col int) fyne.Position {
4✔
144
        y := float32(row) * t.content.cellSize.Height
4✔
145
        x := float32(col) * t.content.cellSize.Width
4✔
146

4✔
147
        if t.scroll != nil && t.scroll.Visible() {
8✔
148
                y -= t.scroll.Offset.Y
4✔
149
                x -= t.scroll.Offset.X
4✔
150
        }
4✔
151

152
        return fyne.NewPos(x, y)
4✔
153
}
154

155
// MinSize returns the smallest size this widget can shrink to
156
func (t *TextGrid) MinSize() fyne.Size {
20✔
157
        t.ExtendBaseWidget(t)
20✔
158
        return t.BaseWidget.MinSize()
20✔
159
}
20✔
160

161
// Resize is called when this widget changes size. We should make sure that we refresh cells.
162
func (t *TextGrid) Resize(size fyne.Size) {
40✔
163
        t.BaseWidget.Resize(size)
40✔
164
        t.Refresh()
40✔
165
}
40✔
166

167
// SetText updates the buffer of this textgrid to contain the specified text.
168
// New lines and columns will be added as required. Lines are separated by '\n'.
169
// The grid will use default text style and any previous content and style will be removed.
170
// Tab characters are padded with spaces to the next tab stop.
171
func (t *TextGrid) SetText(text string) {
32✔
172
        rows := t.parseRows(text)
32✔
173

32✔
174
        oldRowsLen := len(t.Rows)
32✔
175
        t.Rows = rows
32✔
176

32✔
177
        // If we don't update the scroll offset when the text is shorter,
32✔
178
        // we may end up with no text displayed or text appearing partially cut off
32✔
179
        if t.scroll != nil && t.Scroll != fyne.ScrollNone && len(rows) < oldRowsLen && t.scroll.Content != nil {
32✔
180
                offset := t.PositionForCursorLocation(len(rows), 0)
×
181
                t.scroll.ScrollToOffset(fyne.NewPos(offset.X, t.scroll.Offset.Y))
×
182
                t.scroll.Refresh()
×
183
        }
×
184

185
        t.Refresh()
32✔
186
}
187

188
// Text returns the contents of the buffer as a single string (with no style information).
189
// It reconstructs the lines by joining with a `\n` character.
190
// Tab characters have padded spaces removed.
191
func (t *TextGrid) Text() string {
4✔
192
        count := len(t.Rows) - 1 // newlines
4✔
193
        for _, row := range t.Rows {
11✔
194
                count += len(row.Cells)
7✔
195
        }
7✔
196

197
        if count <= 0 {
5✔
198
                return ""
1✔
199
        }
1✔
200

201
        runes := make([]rune, 0, count)
3✔
202

3✔
203
        for i, row := range t.Rows {
10✔
204
                next := 0
7✔
205
                for col, cell := range row.Cells {
43✔
206
                        if col < next {
39✔
207
                                continue
3✔
208
                        }
209
                        runes = append(runes, cell.Rune)
33✔
210
                        if cell.Rune == '\t' {
34✔
211
                                next = nextTab(col, t.tabWidth())
1✔
212
                        }
1✔
213
                }
214
                if i < len(t.Rows)-1 {
11✔
215
                        runes = append(runes, '\n')
4✔
216
                }
4✔
217
        }
218

219
        return string(runes)
3✔
220
}
221

222
// Row returns a copy of the content in a specified row as a TextGridRow.
223
// If the index is out of bounds it returns an empty row object.
224
func (t *TextGrid) Row(row int) TextGridRow {
5✔
225
        if row < 0 || row >= len(t.Rows) {
5✔
226
                return TextGridRow{}
×
227
        }
×
228

229
        return t.Rows[row]
5✔
230
}
231

232
// RowText returns a string representation of the content at the row specified.
233
// If the index is out of bounds it returns an empty string.
234
func (t *TextGrid) RowText(row int) string {
2✔
235
        rowData := t.Row(row)
2✔
236
        count := len(rowData.Cells)
2✔
237

2✔
238
        if count <= 0 {
2✔
239
                return ""
×
240
        }
×
241

242
        runes := make([]rune, 0, count)
2✔
243

2✔
244
        next := 0
2✔
245
        for col, cell := range rowData.Cells {
5✔
246
                if col < next {
3✔
247
                        continue
×
248
                }
249
                runes = append(runes, cell.Rune)
3✔
250
                if cell.Rune == '\t' {
3✔
251
                        next = nextTab(col, t.tabWidth())
×
252
                }
×
253
        }
254
        return string(runes)
2✔
255
}
256

257
// SetRow updates the specified row of the grid's contents using the specified content and style and then refreshes.
258
// If the row is beyond the end of the current buffer it will be expanded.
259
// Tab characters are not padded with spaces.
260
func (t *TextGrid) SetRow(row int, content TextGridRow) {
×
261
        if row < 0 {
×
262
                return
×
263
        }
×
264
        for len(t.Rows) <= row {
×
265
                t.Rows = append(t.Rows, TextGridRow{})
×
266
        }
×
267

268
        t.Rows[row] = content
×
269
        for col := 0; col > len(content.Cells); col++ {
×
270
                t.refreshCell(row, col)
×
271
        }
×
272
}
273

274
// SetRowStyle sets a grid style to all the cells cell at the specified row.
275
// Any cells in this row with their own style will override this value when displayed.
276
func (t *TextGrid) SetRowStyle(row int, style TextGridStyle) {
1✔
277
        if row < 0 {
1✔
278
                return
×
279
        }
×
280
        for len(t.Rows) <= row {
1✔
281
                t.Rows = append(t.Rows, TextGridRow{})
×
282
        }
×
283
        t.Rows[row].Style = style
1✔
284
}
285

286
// SetCell sets a grid data to the cell at named row and column.
287
func (t *TextGrid) SetCell(row, col int, cell TextGridCell) {
×
288
        if row < 0 || col < 0 {
×
289
                return
×
290
        }
×
291
        t.ensureCells(row, col)
×
292

×
293
        t.Rows[row].Cells[col] = cell
×
294
        t.refreshCell(row, col)
×
295
}
296

297
// SetRune sets a character to the cell at named row and column.
298
func (t *TextGrid) SetRune(row, col int, r rune) {
×
299
        if row < 0 || col < 0 {
×
300
                return
×
301
        }
×
302
        t.ensureCells(row, col)
×
303

×
304
        t.Rows[row].Cells[col].Rune = r
×
305
        t.refreshCell(row, col)
×
306
}
307

308
// SetStyle sets a grid style to the cell at named row and column.
309
func (t *TextGrid) SetStyle(row, col int, style TextGridStyle) {
7✔
310
        if row < 0 || col < 0 {
7✔
311
                return
×
312
        }
×
313
        t.ensureCells(row, col)
7✔
314

7✔
315
        t.Rows[row].Cells[col].Style = style
7✔
316
        t.refreshCell(row, col)
7✔
317
}
318

319
// SetStyleRange sets a grid style to all the cells between the start row and column through to the end row and column.
320
func (t *TextGrid) SetStyleRange(startRow, startCol, endRow, endCol int, style TextGridStyle) {
5✔
321
        if startRow >= len(t.Rows) || endRow < 0 {
7✔
322
                return
2✔
323
        }
2✔
324
        if startRow < 0 {
4✔
325
                startRow = 0
1✔
326
                startCol = 0
1✔
327
        }
1✔
328
        if endRow >= len(t.Rows) {
4✔
329
                endRow = len(t.Rows) - 1
1✔
330
                endCol = len(t.Rows[endRow].Cells) - 1
1✔
331
        }
1✔
332

333
        if startRow == endRow {
5✔
334
                for col := startCol; col <= endCol; col++ {
4✔
335
                        t.SetStyle(startRow, col, style)
2✔
336
                }
2✔
337
                return
2✔
338
        }
339

340
        // first row
341
        for col := startCol; col < len(t.Rows[startRow].Cells); col++ {
2✔
342
                t.SetStyle(startRow, col, style)
1✔
343
        }
1✔
344

345
        // possible middle rows
346
        for rowNum := startRow + 1; rowNum < endRow; rowNum++ {
2✔
347
                for col := 0; col < len(t.Rows[rowNum].Cells); col++ {
3✔
348
                        t.SetStyle(rowNum, col, style)
2✔
349
                }
2✔
350
        }
351

352
        // last row
353
        for col := 0; col <= endCol; col++ {
2✔
354
                t.SetStyle(endRow, col, style)
1✔
355
        }
1✔
356
}
357

358
// CreateRenderer is a private method to Fyne which links this widget to its renderer
359
func (t *TextGrid) CreateRenderer() fyne.WidgetRenderer {
32✔
360
        t.ExtendBaseWidget(t)
32✔
361

32✔
362
        th := t.Theme()
32✔
363
        v := fyne.CurrentApp().Settings().ThemeVariant()
32✔
364
        TextGridStyleDefault = &CustomTextGridStyle{}
32✔
365
        TextGridStyleWhitespace = &CustomTextGridStyle{FGColor: th.Color(theme.ColorNameDisabled, v)}
32✔
366

32✔
367
        var scroll *widget.Scroll
32✔
368
        content := newTextGridContent(t)
32✔
369
        objs := make([]fyne.CanvasObject, 1)
32✔
370
        if t.Scroll == widget.ScrollNone {
61✔
371
                scroll = widget.NewScroll(nil)
29✔
372
                objs[0] = content
29✔
373
        } else {
32✔
374
                scroll = widget.NewScroll(content)
3✔
375
                scroll.Direction = t.Scroll
3✔
376
                objs[0] = scroll
3✔
377
        }
3✔
378
        t.scroll = scroll
32✔
379
        t.content = content
32✔
380
        r := &textGridRenderer{text: content, scroll: scroll}
32✔
381
        r.SetObjects(objs)
32✔
382
        return r
32✔
383
}
384

385
func (t *TextGrid) ensureCells(row, col int) {
7✔
386
        for len(t.Rows) <= row {
7✔
387
                t.Rows = append(t.Rows, TextGridRow{})
×
388
        }
×
389
        data := t.Rows[row]
7✔
390

7✔
391
        for len(data.Cells) <= col {
7✔
392
                data.Cells = append(data.Cells, TextGridCell{})
×
393
                t.Rows[row] = data
×
394
        }
×
395
}
396

397
func (t *TextGrid) parseRows(text string) []TextGridRow {
33✔
398
        lines := strings.Split(text, "\n")
33✔
399
        rows := make([]TextGridRow, len(lines))
33✔
400
        for i, line := range lines {
110✔
401
                cells := make([]TextGridCell, 0, len(line))
77✔
402
                for _, r := range line {
386✔
403
                        cells = append(cells, TextGridCell{Rune: r})
309✔
404
                        if r == '\t' {
310✔
405
                                col := len(cells)
1✔
406
                                next := nextTab(col-1, t.tabWidth())
1✔
407
                                for i := col; i < next; i++ {
4✔
408
                                        cells = append(cells, TextGridCell{Rune: ' '})
3✔
409
                                }
3✔
410
                        }
411
                }
412
                rows[i] = TextGridRow{Cells: cells}
77✔
413
        }
414

415
        return rows
33✔
416
}
417

418
func (t *TextGrid) refreshCell(row, col int) {
7✔
419
        r := t.content
7✔
420
        r.refreshCell(row, col)
7✔
421
}
7✔
422

423
// NewTextGrid creates a new empty TextGrid widget.
424
func NewTextGrid() *TextGrid {
32✔
425
        grid := &TextGrid{}
32✔
426
        grid.Scroll = widget.ScrollNone
32✔
427
        grid.ExtendBaseWidget(grid)
32✔
428
        return grid
32✔
429
}
32✔
430

431
// NewTextGridFromString creates a new TextGrid widget with the specified string content.
432
func NewTextGridFromString(content string) *TextGrid {
24✔
433
        grid := NewTextGrid()
24✔
434
        grid.SetText(content)
24✔
435
        return grid
24✔
436
}
24✔
437

438
// nextTab finds the column of the next tab stop for the given column
439
func nextTab(column int, tabWidth int) int {
2✔
440
        tabStop, _ := math.Modf(float64(column+tabWidth) / float64(tabWidth))
2✔
441
        return tabWidth * int(tabStop)
2✔
442
}
2✔
443

444
type textGridRenderer struct {
445
        widget.BaseRenderer
446

447
        text   *textGridContent
448
        scroll *widget.Scroll
449
}
450

451
func (t *textGridRenderer) Layout(s fyne.Size) {
35✔
452
        t.Objects()[0].Resize(s)
35✔
453
}
35✔
454

455
func (t *textGridRenderer) MinSize() fyne.Size {
22✔
456
        if t.text.text.Scroll == widget.ScrollNone {
32✔
457
                return t.text.MinSize()
10✔
458
        }
10✔
459

460
        return t.scroll.MinSize()
12✔
461
}
462

463
func (t *textGridRenderer) Refresh() {
85✔
464
        content := t.text
85✔
465
        if t.text.text.Scroll != widget.ScrollNone {
114✔
466
                t.scroll.Direction = t.text.text.Scroll
29✔
467
        }
29✔
468
        if t.text.text.Scroll == widget.ScrollNone && t.scroll.Content != nil {
86✔
469
                t.scroll.Hide()
1✔
470
                t.scroll.Content = nil
1✔
471
                content.Resize(t.text.Size())
1✔
472
                t.SetObjects([]fyne.CanvasObject{t.text})
1✔
473
        } else if (t.text.text.Scroll != widget.ScrollNone) && t.scroll.Content == nil {
89✔
474
                t.scroll.Content = content
4✔
475
                t.scroll.Show()
4✔
476

4✔
477
                t.scroll.Resize(t.text.Size())
4✔
478
                content.Resize(content.MinSize())
4✔
479
                t.SetObjects([]fyne.CanvasObject{t.scroll})
4✔
480
        }
4✔
481

482
        canvas.Refresh(t.text.text.super())
85✔
483
        t.text.Refresh()
85✔
484
}
485

486
type textGridContent struct {
487
        BaseWidget
488
        text *TextGrid
489

490
        rows     int
491
        cellSize fyne.Size
492

493
        visible []fyne.CanvasObject
494
}
495

496
func newTextGridContent(t *TextGrid) *textGridContent {
32✔
497
        grid := &textGridContent{text: t}
32✔
498
        grid.ExtendBaseWidget(grid)
32✔
499
        return grid
32✔
500
}
32✔
501

502
// CreateRenderer is a private method to Fyne which links this widget to its renderer
503
func (t *textGridContent) CreateRenderer() fyne.WidgetRenderer {
32✔
504
        r := &textGridContentRenderer{text: t}
32✔
505

32✔
506
        r.updateCellSize()
32✔
507
        t.text.scroll.OnScrolled = func(_ fyne.Position) {
34✔
508
                r.addRowsIfRequired()
2✔
509
                r.Layout(t.Size())
2✔
510
        }
2✔
511
        return r
32✔
512
}
513

514
func (t *textGridContent) refreshCell(row, col int) {
7✔
515
        if row >= len(t.visible)-1 {
7✔
516
                return
×
517
        }
×
518
        wid := t.visible[row].(*textGridRow)
7✔
519
        wid.refreshCell(col)
7✔
520
}
521

522
type textGridContentRenderer struct {
523
        text     *textGridContent
524
        itemPool async.Pool[*textGridRow]
525
}
526

527
func (t *textGridContentRenderer) updateGridSize(size fyne.Size) {
116✔
528
        bufRows := len(t.text.text.Rows)
116✔
529
        sizeRows := int(size.Height / t.text.cellSize.Height)
116✔
530

116✔
531
        t.text.rows = max(sizeRows, bufRows)
116✔
532
        t.addRowsIfRequired()
116✔
533
}
116✔
534

535
func (t *textGridContentRenderer) Destroy() {
1✔
536
}
1✔
537

538
func (t *textGridContentRenderer) Layout(s fyne.Size) {
31✔
539
        size := fyne.NewSize(s.Width, t.text.cellSize.Height)
31✔
540
        t.updateGridSize(s)
31✔
541

31✔
542
        for _, o := range t.text.visible {
143✔
543
                o.Move(fyne.NewPos(0, float32(o.(*textGridRow).row)*t.text.cellSize.Height))
112✔
544
                o.Resize(size)
112✔
545
        }
112✔
546
}
547

548
func (t *textGridContentRenderer) MinSize() fyne.Size {
113✔
549
        longestRow := float32(0)
113✔
550
        for _, row := range t.text.text.Rows {
317✔
551
                longestRow = max(longestRow, float32(len(row.Cells)))
204✔
552
        }
204✔
553
        return fyne.NewSize(t.text.cellSize.Width*longestRow,
113✔
554
                t.text.cellSize.Height*float32(len(t.text.text.Rows)))
113✔
555
}
556

557
func (t *textGridContentRenderer) Objects() []fyne.CanvasObject {
10✔
558
        return t.text.visible
10✔
559
}
10✔
560

561
func (t *textGridContentRenderer) Refresh() {
85✔
562
        // theme could change text size
85✔
563
        t.updateCellSize()
85✔
564
        t.updateGridSize(t.text.text.Size())
85✔
565

85✔
566
        for _, o := range t.text.visible {
372✔
567
                o.Refresh()
287✔
568
        }
287✔
569
}
570

571
func (t *textGridContentRenderer) addRowsIfRequired() {
118✔
572
        start := 0
118✔
573
        end := t.text.rows
118✔
574
        if t.text.text.Scroll == widget.ScrollBoth || t.text.text.Scroll == widget.ScrollVerticalOnly {
164✔
575
                off := t.text.text.scroll.Offset.Y
46✔
576
                start = int(math.Floor(float64(off / t.text.cellSize.Height)))
46✔
577

46✔
578
                off += t.text.text.Size().Height
46✔
579
                end = int(math.Ceil(float64(off / t.text.cellSize.Height)))
46✔
580
        }
46✔
581

582
        remain := t.text.visible[:0]
118✔
583
        for _, row := range t.text.visible {
398✔
584
                if row.(*textGridRow).row < start || row.(*textGridRow).row > end {
283✔
585
                        t.itemPool.Put(row.(*textGridRow))
3✔
586
                        continue
3✔
587
                }
588

589
                remain = append(remain, row.(*textGridRow))
277✔
590
        }
591
        t.text.visible = remain
118✔
592

118✔
593
        var newItems []fyne.CanvasObject
118✔
594
        for i := start; i <= end; i++ {
523✔
595
                found := false
405✔
596
                for _, row := range t.text.visible {
1,319✔
597
                        if i == row.(*textGridRow).row {
1,191✔
598
                                found = true
277✔
599
                                break
277✔
600
                        }
601
                }
602

603
                if found {
682✔
604
                        continue
277✔
605
                }
606

607
                newRow := t.itemPool.Get()
128✔
608
                if newRow == nil {
256✔
609
                        newRow = newTextGridRow(t.text, i)
128✔
610
                } else {
128✔
611
                        newRow.setRow(i)
×
612
                }
×
613
                newItems = append(newItems, newRow)
128✔
614
        }
615

616
        if len(newItems) > 0 {
154✔
617
                t.text.visible = append(t.text.visible, newItems...)
36✔
618
        }
36✔
619
}
620

621
func (t *textGridContentRenderer) updateCellSize() {
117✔
622
        th := t.text.Theme()
117✔
623
        size := fyne.MeasureText("M", th.Size(theme.SizeNameText), fyne.TextStyle{Monospace: true})
117✔
624

117✔
625
        // round it for seamless background
117✔
626
        size.Width = float32(math.Round(float64(size.Width)))
117✔
627
        size.Height = float32(math.Round(float64(size.Height)))
117✔
628

117✔
629
        t.text.cellSize = size
117✔
630
}
117✔
631

632
type textGridRow struct {
633
        BaseWidget
634
        text *textGridContent
635

636
        objects []fyne.CanvasObject
637
        row     int
638
        cols    int
639

640
        cachedFGColor  color.Color
641
        cachedTextSize float32
642
}
643

644
func newTextGridRow(t *textGridContent, row int) *textGridRow {
128✔
645
        newRow := &textGridRow{text: t, row: row}
128✔
646
        newRow.ExtendBaseWidget(newRow)
128✔
647

128✔
648
        return newRow
128✔
649
}
128✔
650

651
// CreateRenderer is a private method to Fyne which links this widget to its renderer
652
func (t *textGridRow) CreateRenderer() fyne.WidgetRenderer {
128✔
653
        render := &textGridRowRenderer{obj: t}
128✔
654

128✔
655
        render.Refresh() // populate
128✔
656
        return render
128✔
657
}
128✔
658

659
func (t *textGridRow) setRow(row int) {
×
660
        t.row = row
×
661
        t.Refresh()
×
662
}
×
663

664
func (t *textGridRow) appendTextCell(str rune) {
1,062✔
665
        th := t.text.text.Theme()
1,062✔
666
        v := fyne.CurrentApp().Settings().ThemeVariant()
1,062✔
667

1,062✔
668
        text := canvas.NewText(string(str), th.Color(theme.ColorNameForeground, v))
1,062✔
669
        text.TextStyle.Monospace = true
1,062✔
670

1,062✔
671
        bg := canvas.NewRectangle(color.Transparent)
1,062✔
672

1,062✔
673
        ul := canvas.NewLine(color.Transparent)
1,062✔
674

1,062✔
675
        t.objects = append(t.objects, bg, text, ul)
1,062✔
676
}
1,062✔
677

678
func (t *textGridRow) refreshCell(col int) {
7✔
679
        pos := t.cols + col
7✔
680
        if pos*3+1 >= len(t.objects) {
14✔
681
                return
7✔
682
        }
7✔
683

684
        row := t.text.text.Rows[t.row]
×
685

×
686
        if len(row.Cells) > col {
×
687
                cell := row.Cells[col]
×
688
                t.setCellRune(cell.Rune, pos, cell.Style, row.Style)
×
689
        }
×
690
}
691

692
func (t *textGridRow) setCellRune(str rune, pos int, style, rowStyle TextGridStyle) {
2,988✔
693
        if str == 0 {
2,988✔
694
                str = ' '
×
695
        }
×
696
        rect := t.objects[pos*3].(*canvas.Rectangle)
2,988✔
697
        text := t.objects[pos*3+1].(*canvas.Text)
2,988✔
698
        underline := t.objects[pos*3+2].(*canvas.Line)
2,988✔
699

2,988✔
700
        fg := t.cachedFGColor
2,988✔
701
        text.TextSize = t.cachedTextSize
2,988✔
702

2,988✔
703
        var underlineStrokeWidth float32 = 1
2,988✔
704
        var underlineStrokeColor color.Color = color.Transparent
2,988✔
705
        textStyle := fyne.TextStyle{}
2,988✔
706
        if style != nil {
4,964✔
707
                textStyle = style.Style()
1,976✔
708
        } else if rowStyle != nil {
2,990✔
709
                textStyle = rowStyle.Style()
2✔
710
        }
2✔
711
        if textStyle.Bold {
2,990✔
712
                underlineStrokeWidth = 2
2✔
713
        }
2✔
714
        if textStyle.Underline {
2,988✔
715
                underlineStrokeColor = fg
×
716
        }
×
717
        textStyle.Monospace = true
2,988✔
718

2,988✔
719
        if style != nil && style.TextColor() != nil {
3,024✔
720
                fg = style.TextColor()
36✔
721
        } else if rowStyle != nil && rowStyle.TextColor() != nil {
2,995✔
722
                fg = rowStyle.TextColor()
7✔
723
        }
7✔
724

725
        newStr := string(str)
2,988✔
726
        if text.Text != newStr || text.Color != fg || textStyle != text.TextStyle {
3,373✔
727
                text.Text = newStr
385✔
728
                text.Color = fg
385✔
729
                text.TextStyle = textStyle
385✔
730
                text.Refresh()
385✔
731
        }
385✔
732

733
        if underlineStrokeWidth != underline.StrokeWidth || underlineStrokeColor != underline.StrokeColor {
2,990✔
734
                underline.StrokeWidth, underline.StrokeColor = underlineStrokeWidth, underlineStrokeColor
2✔
735
                underline.Refresh()
2✔
736
        }
2✔
737

738
        bg := color.Color(color.Transparent)
2,988✔
739
        if style != nil && style.BackgroundColor() != nil {
2,988✔
740
                bg = style.BackgroundColor()
×
741
        } else if rowStyle != nil && rowStyle.BackgroundColor() != nil {
2,988✔
742
                bg = rowStyle.BackgroundColor()
×
743
        }
×
744
        if rect.FillColor != bg {
2,988✔
745
                rect.FillColor = bg
×
746
                rect.Refresh()
×
747
        }
×
748
}
749

750
func (t *textGridRow) addCellsIfRequired() {
521✔
751
        cellCount := t.cols
521✔
752
        if len(t.objects) == cellCount*3 {
874✔
753
                return
353✔
754
        }
353✔
755
        for i := len(t.objects); i < cellCount*3; i += 3 {
1,230✔
756
                t.appendTextCell(' ')
1,062✔
757
        }
1,062✔
758
}
759

760
func (t *textGridRow) refreshCells() {
415✔
761
        x := 0
415✔
762
        if t.row >= len(t.text.text.Rows) {
589✔
763
                for ; x < len(t.objects)/3; x++ {
1,558✔
764
                        t.setCellRune(' ', x, TextGridStyleDefault, nil) // blank rows no longer needed
1,384✔
765
                }
1,384✔
766

767
                return // we can have more rows than content rows (filling space)
174✔
768
        }
769

770
        row := t.text.text.Rows[t.row]
241✔
771
        rowStyle := row.Style
241✔
772
        i := 0
241✔
773
        if t.text.text.ShowLineNumbers {
253✔
774
                lineStr := []rune(strconv.Itoa(t.row + 1))
12✔
775
                pad := t.lineNumberWidth() - len(lineStr)
12✔
776
                for ; i < pad; i++ {
23✔
777
                        t.setCellRune(' ', x, TextGridStyleWhitespace, rowStyle) // padding space
11✔
778
                        x++
11✔
779
                }
11✔
780
                for c := range lineStr {
25✔
781
                        t.setCellRune(lineStr[c], x, TextGridStyleDefault, rowStyle) // line numbers
13✔
782
                        i++
13✔
783
                        x++
13✔
784
                }
13✔
785

786
                t.setCellRune('|', x, TextGridStyleWhitespace, rowStyle) // last space
12✔
787
                i++
12✔
788
                x++
12✔
789
        }
790
        for _, r := range row.Cells {
1,267✔
791
                if i >= t.cols { // would be an overflow - bad
1,026✔
792
                        continue
×
793
                }
794
                if t.text.text.ShowWhitespace && (r.Rune == ' ' || r.Rune == '\t') {
1,035✔
795
                        sym := textAreaSpaceSymbol
9✔
796
                        if r.Rune == '\t' {
10✔
797
                                sym = textAreaTabSymbol
1✔
798
                        }
1✔
799

800
                        if r.Style != nil && r.Style.BackgroundColor() != nil {
9✔
801
                                whitespaceBG := &CustomTextGridStyle{
×
802
                                        FGColor: TextGridStyleWhitespace.TextColor(),
×
803
                                        BGColor: r.Style.BackgroundColor(),
×
804
                                }
×
805
                                t.setCellRune(sym, x, whitespaceBG, rowStyle) // whitespace char
×
806
                        } else {
9✔
807
                                t.setCellRune(sym, x, TextGridStyleWhitespace, rowStyle) // whitespace char
9✔
808
                        }
9✔
809
                } else {
1,017✔
810
                        t.setCellRune(r.Rune, x, r.Style, rowStyle) // regular char
1,017✔
811
                }
1,017✔
812
                i++
1,026✔
813
                x++
1,026✔
814
        }
815
        if t.text.text.ShowWhitespace && i < t.cols && t.row < len(t.text.text.Rows)-1 {
243✔
816
                t.setCellRune(textAreaNewLineSymbol, x, TextGridStyleWhitespace, rowStyle) // newline
2✔
817
                i++
2✔
818
                x++
2✔
819
        }
2✔
820
        for ; i < t.cols; i++ {
774✔
821
                t.setCellRune(' ', x, TextGridStyleDefault, rowStyle) // blanks
533✔
822
                x++
533✔
823
        }
533✔
824

825
        for ; x < len(t.objects)/3; x++ {
248✔
826
                t.setCellRune(' ', x, TextGridStyleDefault, nil) // trailing cells and blank lines
7✔
827
        }
7✔
828
}
829

830
// tabWidth either returns the set tab width or if not set the returns the DefaultTabWidth
831
func (t *TextGrid) tabWidth() int {
2✔
832
        if t.TabWidth == 0 {
4✔
833
                return painter.DefaultTabWidth
2✔
834
        }
2✔
835
        return t.TabWidth
×
836
}
837

838
func (t *textGridRow) lineNumberWidth() int {
94✔
839
        return len(strconv.Itoa(t.text.rows + 1))
94✔
840
}
94✔
841

842
func (t *textGridRow) updateGridSize(size fyne.Size) {
521✔
843
        bufCols := int(size.Width / t.text.cellSize.Width)
521✔
844
        for _, row := range t.text.text.Rows {
2,196✔
845
                lenCells := len(row.Cells)
1,675✔
846
                if lenCells > bufCols {
1,990✔
847
                        bufCols = lenCells
315✔
848
                }
315✔
849
        }
850

851
        if t.text.text.ShowWhitespace {
547✔
852
                bufCols++
26✔
853
        }
26✔
854
        if t.text.text.ShowLineNumbers {
603✔
855
                bufCols += t.lineNumberWidth()
82✔
856
        }
82✔
857

858
        t.cols = bufCols
521✔
859
        t.addCellsIfRequired()
521✔
860
}
861

862
type textGridRowRenderer struct {
863
        obj *textGridRow
864
}
865

866
func (t *textGridRowRenderer) Layout(size fyne.Size) {
106✔
867
        t.obj.updateGridSize(size)
106✔
868

106✔
869
        cellPos := fyne.NewPos(0, 0)
106✔
870
        off := 0
106✔
871
        for x := 0; x < t.obj.cols; x++ {
1,193✔
872
                // rect
1,087✔
873
                t.obj.objects[off].Resize(t.obj.text.cellSize)
1,087✔
874
                t.obj.objects[off].Move(cellPos)
1,087✔
875

1,087✔
876
                // text
1,087✔
877
                t.obj.objects[off+1].Move(cellPos)
1,087✔
878

1,087✔
879
                // underline
1,087✔
880
                t.obj.objects[off+2].Move(cellPos.Add(fyne.Position{X: 0, Y: t.obj.text.cellSize.Height}))
1,087✔
881
                t.obj.objects[off+2].Resize(fyne.Size{Width: t.obj.text.cellSize.Width})
1,087✔
882

1,087✔
883
                cellPos.X += t.obj.text.cellSize.Width
1,087✔
884
                off += 3
1,087✔
885
        }
1,087✔
886
}
887

888
func (t *textGridRowRenderer) MinSize() fyne.Size {
×
889
        longestRow := float32(0)
×
890
        for _, row := range t.obj.text.text.Rows {
×
NEW
891
                longestRow = max(longestRow, float32(len(row.Cells)))
×
892
        }
×
893
        return fyne.NewSize(t.obj.text.cellSize.Width*longestRow, t.obj.text.cellSize.Height)
×
894
}
895

896
func (t *textGridRowRenderer) Refresh() {
415✔
897
        th := t.obj.text.text.Theme()
415✔
898
        v := fyne.CurrentApp().Settings().ThemeVariant()
415✔
899
        t.obj.cachedFGColor = th.Color(theme.ColorNameForeground, v)
415✔
900
        t.obj.cachedTextSize = th.Size(theme.SizeNameText)
415✔
901
        TextGridStyleWhitespace = &CustomTextGridStyle{FGColor: th.Color(theme.ColorNameDisabled, v)}
415✔
902
        t.obj.updateGridSize(t.obj.text.text.Size())
415✔
903
        t.obj.refreshCells()
415✔
904
}
415✔
905

906
func (t *textGridRowRenderer) ApplyTheme() {
×
907
}
×
908

909
func (t *textGridRowRenderer) Objects() []fyne.CanvasObject {
30✔
910
        return t.obj.objects
30✔
911
}
30✔
912

913
func (t *textGridRowRenderer) Destroy() {
17✔
914
}
17✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc