Skip to content

Commit

Permalink
Add color information to the textgrid
Browse files Browse the repository at this point in the history
Upgrade the [][]rune to [][]TextGridCell, more content to appear
  • Loading branch information
andydotxyz committed Mar 2, 2020
1 parent e0cb736 commit 2060e2e
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 61 deletions.
121 changes: 77 additions & 44 deletions widget/textgrid.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,22 @@ const (
textAreaNewLineSymbol = '↵'
)

// TextGridCell represents a single cell in a text grid.
// It has a rune for the text content and a style associated with it.
type TextGridCell struct {
Rune rune
TextColor color.Color
}

var (
whitespaceColor = theme.ButtonColor()
)

// TextGrid is a monospaced grid of characters.
// This is designed to be used by a text editor or advanced test presentation.
// This is designed to be used by a text editor, code preview or terminal emulator.
type TextGrid struct {
BaseWidget
Buffer [][]rune
Content [][]TextGridCell

LineNumbers bool
Whitespace bool
Expand All @@ -33,62 +44,75 @@ func (t *TextGrid) MinSize() fyne.Size {
return t.BaseWidget.MinSize()
}

// Resize is called when this widget changes size. We should make sure that we refresh cells.
func (t *TextGrid) Resize(size fyne.Size) {
t.BaseWidget.Resize(size)
t.Refresh()
}

// SetText updates the buffer of this textgrid to contain the specified text.
// New lines and columns will be added as required. Lines are separated by '\n'.
// The grid will use default text style and any previous content and style will be removed.
func (t *TextGrid) SetText(text string) {
rows := strings.Split(text, "\n")
var buffer [][]rune
for _, row := range rows {
buffer = append(buffer, []rune(row))
var buffer [][]TextGridCell
for _, runes := range rows {
var row []TextGridCell
for _, r := range runes {
row = append(row, TextGridCell{Rune: r})
}
buffer = append(buffer, row)
}

t.Buffer = buffer
t.Content = buffer
t.Refresh()
}

// Text returns the contents of the buffer as a single string.
// Text returns the contents of the buffer as a single string (with no style information).
// It reconstructs the lines by joining with a `\n` character.
func (t *TextGrid) Text() string {
ret := ""
for i, row := range t.Buffer {
ret += string(row)
for i, row := range t.Content {
for _, r := range row {
ret += string(r.Rune)
}

if i < len(t.Buffer)-1 {
if i < len(t.Content)-1 {
ret += "\n"
}
}

return ret
}

// Row returns the []rune content of a specified row. If the index is out of bounds it returns an empty slice.
func (t *TextGrid) Row(row int) []rune {
if row < 0 || row >= len(t.Buffer) {
return []rune{}
// Row returns the content of a specified row as a slice of TextGridCells.
// If the index is out of bounds it returns an empty slice.
func (t *TextGrid) Row(row int) []TextGridCell {
if row < 0 || row >= len(t.Content) {
return []TextGridCell{}
}

return t.Buffer[row]
return t.Content[row]
}

// SetRow updates the specified row of the grid's buffer using the specified content and then refreshes.
// SetRow updates the specified row of the grid's contents using the specified cell content and style and then refreshes.
// If the row is beyond the end of the current buffer it will be expanded.
func (t *TextGrid) SetRow(row int, content []rune) {
func (t *TextGrid) SetRow(row int, content []TextGridCell) {
if row < 0 {
return
}
for len(t.Buffer) <= row {
t.Buffer = append(t.Buffer, []rune{})
for len(t.Content) <= row {
t.Content = append(t.Content, []TextGridCell{})
}

t.Buffer[row] = content
t.Content[row] = content
t.Refresh()
}

// CreateRenderer is a private method to Fyne which links this widget to it's renderer
func (t *TextGrid) CreateRenderer() fyne.WidgetRenderer {
t.ExtendBaseWidget(t)
render := &textGridRender{text: t}
render.update()

cell := canvas.NewText("M", color.White)
cell.TextStyle.Monospace = true
Expand Down Expand Up @@ -127,20 +151,20 @@ func (t *textGridRender) appendTextCell(str rune) {
t.objects = append(t.objects, text)
}

func (t *textGridRender) setCellRune(str rune, pos int) {
func (t *textGridRender) setCellRune(str rune, pos int, cellFG color.Color) {
text := t.objects[pos].(*canvas.Text)
text.Text = string(str)

if str == textAreaSpaceSymbol || str == textAreaTabSymbol || str == textAreaNewLineSymbol {
text.Color = theme.PlaceHolderColor()
if str == 0 {
text.Text = " "
} else {
text.Color = theme.TextColor()
text.Text = string(str)
}
}

func (t *textGridRender) update() {
t.ensureGrid()
t.refreshGrid()
fg := theme.TextColor()
if cellFG != nil {
fg = cellFG
}

text.Color = fg
}

func (t *textGridRender) ensureGrid() {
Expand All @@ -157,52 +181,53 @@ func (t *textGridRender) refreshGrid() {
line := 1
x := 0

for rowIndex, row := range t.text.Buffer {
for rowIndex, row := range t.text.Content {
if rowIndex >= t.rows { // would be an overflow - bad
break
}
i := 0
if t.text.LineNumbers {
lineStr := []rune(fmt.Sprintf("%d", line))
for c := 0; c < len(lineStr); c++ {
t.setCellRune(lineStr[c], x)
t.setCellRune(lineStr[c], x, whitespaceColor) // line numbers
i++
x++
}
for ; i < t.lineCountWidth(); i++ {
t.setCellRune(' ', x)
t.setCellRune(' ', x, whitespaceColor) // padding space
x++
}

t.setCellRune(' ', x)
t.setCellRune('|', x, whitespaceColor) // last space
i++
x++
}
for _, r := range row {
if i >= t.cols { // would be an overflow - bad
continue
}
if t.text.Whitespace && r == ' ' {
r = textAreaSpaceSymbol
if t.text.Whitespace && r.Rune == ' ' {
t.setCellRune(textAreaSpaceSymbol, x, whitespaceColor) // whitespace char
} else {
t.setCellRune(r.Rune, x, r.TextColor) // regular char
}
t.setCellRune(r, x)
i++
x++
}
if t.text.Whitespace && i < t.cols {
t.setCellRune(textAreaNewLineSymbol, x)
if t.text.Whitespace && i < t.cols && rowIndex < len(t.text.Content)-1 {
t.setCellRune(textAreaNewLineSymbol, x, whitespaceColor) // newline
i++
x++
}
for ; i < t.cols; i++ {
t.setCellRune(' ', x)
t.setCellRune(' ', x, nil) // blanks
x++
}

line++
}
for ; x < len(t.objects); x++ {
t.setCellRune(' ', x)
t.setCellRune(' ', x, nil) // blank lines?
}
canvas.Refresh(t.text)
}
Expand All @@ -212,14 +237,21 @@ func (t *textGridRender) lineCountWidth() int {
}

func (t *textGridRender) updateGridSize(size fyne.Size) {
bufRows := len(t.text.Buffer)
bufRows := len(t.text.Content)
bufCols := 0
for _, row := range t.text.Buffer {
for _, row := range t.text.Content {
bufCols = int(math.Max(float64(bufCols), float64(len(row))))
}
sizeCols := int(math.Floor(float64(size.Width) / float64(t.cellSize.Width)))
sizeRows := int(math.Floor(float64(size.Height) / float64(t.cellSize.Height)))

if t.text.Whitespace {
bufCols++
}
if t.text.LineNumbers {
bufCols += t.lineCountWidth()
}

t.cols = int(math.Max(float64(sizeCols), float64(bufCols)))
t.rows = int(math.Max(float64(sizeRows), float64(bufRows)))
}
Expand Down Expand Up @@ -248,6 +280,7 @@ func (t *textGridRender) MinSize() fyne.Size {
}

func (t *textGridRender) Refresh() {
t.ensureGrid()
t.refreshGrid()
}

Expand Down
50 changes: 33 additions & 17 deletions widget/textgrid_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package widget

import (
"image/color"
"testing"

"fyne.io/fyne"
"fyne.io/fyne/canvas"
"fyne.io/fyne/test"
"fyne.io/fyne/theme"

"github.com/stretchr/testify/assert"
)
Expand All @@ -13,54 +16,67 @@ func TestNewTextGrid(t *testing.T) {
grid := NewTextGridFromString("A")
Renderer(grid).Refresh()

assert.Equal(t, 1, len(grid.Buffer))
assert.Equal(t, 1, len(grid.Buffer[0]))
assert.Equal(t, 1, len(grid.Content))
assert.Equal(t, 1, len(grid.Content[0]))
}

func TestTextGrid_SetText(t *testing.T) {
grid := NewTextGrid()
grid.SetText("Hello\nthere")

assert.Equal(t, 2, len(grid.Buffer))
assert.Equal(t, 5, len(grid.Buffer[1]))
assert.Equal(t, 2, len(grid.Content))
assert.Equal(t, 5, len(grid.Content[1]))
}

func TestTextGrid_Rows(t *testing.T) {
grid := NewTextGridFromString("Ab\nC")
Renderer(grid).Refresh()
test.WidgetRenderer(grid).Refresh()

assert.Equal(t, 2, len(grid.Buffer))
assert.Equal(t, 2, len(grid.Buffer[0]))
assert.Equal(t, 2, len(grid.Content))
assert.Equal(t, 2, len(grid.Content[0]))
}

func TestTextGrid_CreateRendererRows(t *testing.T) {
grid := NewTextGrid()
grid.Resize(fyne.NewSize(56, 22))
rend := Renderer(grid).(*textGridRender)
rend := test.WidgetRenderer(grid).(*textGridRender)
rend.Refresh()

assert.Equal(t, 4, len(rend.objects))
}

func TestTextGridRender_Size(t *testing.T) {
grid := NewTextGrid()
grid.Resize(fyne.NewSize(32, 42))
rend := Renderer(grid).(*textGridRender)
rend.Refresh()
grid.Resize(fyne.NewSize(32, 42)) // causes refresh
rend := test.WidgetRenderer(grid).(*textGridRender)

assert.Equal(t, 2, rend.cols)
assert.Equal(t, 2, rend.rows)
}

func TestTextGridRender_Whitespace(t *testing.T) {
grid := NewTextGridFromString("A b\nc")
grid.Resize(fyne.NewSize(56, 42))
grid.Whitespace = true
rend := Renderer(grid).(*textGridRender)
rend.Refresh()
grid.Resize(fyne.NewSize(56, 42)) // causes refresh
rend := test.WidgetRenderer(grid).(*textGridRender)

assert.Equal(t, 4, rend.cols)
assert.Equal(t, 2, rend.rows)
assert.Equal(t, string(textAreaSpaceSymbol), rend.objects[1].(*canvas.Text).Text) // col 2 is space
assert.Equal(t, string(textAreaNewLineSymbol), rend.objects[3].(*canvas.Text).Text) // col 4 is newline
assert.NotEqual(t, string(textAreaNewLineSymbol), rend.objects[5].(*canvas.Text).Text) // no newline on end of content
}

func TestTextGridRender_TextColor(t *testing.T) {
grid := NewTextGridFromString("Ab ")
grid.Content[0][1].TextColor = color.Black
grid.Whitespace = true
grid.Resize(fyne.NewSize(56, 22)) // causes refresh
rend := test.WidgetRenderer(grid).(*textGridRender)

assert.Equal(t, 4, rend.cols)
assert.Equal(t, string(textAreaSpaceSymbol), rend.objects[1].(*canvas.Text).Text) // col 2 is space
assert.Equal(t, string(textAreaNewLineSymbol), rend.objects[3].(*canvas.Text).Text) // col 4 is newline
assert.Equal(t, string(textAreaNewLineSymbol), rend.objects[5].(*canvas.Text).Text) // col 2 on line 2
assert.Equal(t, 1, rend.rows)
assert.Equal(t, theme.TextColor(), rend.objects[0].(*canvas.Text).Color)
assert.Equal(t, color.Black, rend.objects[1].(*canvas.Text).Color)
assert.Equal(t, whitespaceColor, rend.objects[2].(*canvas.Text).Color)
}

0 comments on commit 2060e2e

Please sign in to comment.