From cb7580fb5e97a65b4279b3a5ac97cbce191c75a0 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Fri, 13 Mar 2020 22:35:16 +0000 Subject: [PATCH] Add support for backgroun color in textgrid cell. This updates the simple color API to a fuller style API, it should now be easier to extend the style definition in the future. We can also now set semantic styles and not worry about colour definition --- cmd/fyne_demo/screens/widget.go | 6 ++ widget/textgrid.go | 134 ++++++++++++++++++++++++++------ widget/textgrid_test.go | 38 +++++++-- 3 files changed, 147 insertions(+), 31 deletions(-) diff --git a/cmd/fyne_demo/screens/widget.go b/cmd/fyne_demo/screens/widget.go index 99522e5388..d6cb7ddbeb 100644 --- a/cmd/fyne_demo/screens/widget.go +++ b/cmd/fyne_demo/screens/widget.go @@ -2,6 +2,7 @@ package screens import ( "fmt" + "image/color" "time" "fyne.io/fyne" @@ -14,7 +15,12 @@ import ( func makeButtonTab() fyne.Widget { disabled := widget.NewButton("Disabled", func() {}) disabled.Disable() + grid := widget.NewTextGridFromString("TextGrid\n Content") + grid.SetStyleRange(0, 0, 0, 3, + &widget.CustomTextGridStyle{FGColor: color.RGBA{R: 0, G: 0, B: 128, A: 255}}) + grid.SetStyleRange(0, 4, 0, 7, + &widget.CustomTextGridStyle{BGColor: &color.RGBA{R: 128, G: 0, B: 0, A: 255}}) grid.LineNumbers = true grid.Whitespace = true diff --git a/widget/textgrid.go b/widget/textgrid.go index e6b37da579..f64894cc4d 100644 --- a/widget/textgrid.go +++ b/widget/textgrid.go @@ -17,16 +17,46 @@ const ( textAreaNewLineSymbol = '↵' ) +var ( + // TextGridStyleDefault is a default style for test grid cells + TextGridStyleDefault TextGridStyle + // TextGridStyleWhitespace is the style used for whitespace characters, if enabled + TextGridStyleWhitespace TextGridStyle +) + +// define the types seperately to the var definition so the custom style API is not leaked in their instances. +func init() { + TextGridStyleDefault = &CustomTextGridStyle{} + TextGridStyleWhitespace = &CustomTextGridStyle{FGColor: theme.ButtonColor()} +} + // 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 + Rune rune + Style TextGridStyle } -var ( - whitespaceColor = theme.ButtonColor() -) +// TextGridStyle defines a style that can be applied to a TextGrid cell. +type TextGridStyle interface { + TextColor() color.Color + BackgroundColor() color.Color +} + +// CustomTextGridStyle is a utility type for those not wanting to define their own style types. +type CustomTextGridStyle struct { + FGColor, BGColor color.Color +} + +// TextColor is the color a cell should use for the text. +func (c *CustomTextGridStyle) TextColor() color.Color { + return c.FGColor +} + +// BackgroundColor is the color a cell should use for the background. +func (c *CustomTextGridStyle) BackgroundColor() color.Color { + return c.BGColor +} // TextGrid is a monospaced grid of characters. // This is designed to be used by a text editor, code preview or terminal emulator. @@ -109,6 +139,49 @@ func (t *TextGrid) SetRow(row int, content []TextGridCell) { t.Refresh() } +// SetStyle sets a grid style to the cell at named row and column +func (t *TextGrid) SetStyle(row, col int, style TextGridStyle) { + if row < 0 || col < 0 { + return + } + for len(t.Content) <= row { + t.Content = append(t.Content, []TextGridCell{}) + } + content := t.Content[row] + + for len(content) <= col { + content = append(content, TextGridCell{}) + } + content[col].Style = style +} + +// SetStyleRange sets a grid style to all the cells between the start row and column through to the end row and column. +func (t *TextGrid) SetStyleRange(startRow, startCol, endRow, endCol int, style TextGridStyle) { + if startRow == endRow { + for col := startCol; col <= endCol; col++ { + t.SetStyle(startRow, col, style) + } + return + } + + // first row + for col := startCol; col < len(t.Content[startRow]); col++ { + t.SetStyle(startRow, col, style) + } + + // possible middle rows + for rowNum := startRow + 1; rowNum < endRow-1; rowNum++ { + for col := 0; col < len(t.Content[rowNum]); col++ { + t.SetStyle(rowNum, col, style) + } + } + + // last row + for col := 0; col <= endCol; col++ { + t.SetStyle(endRow, col, style) + } +} + // CreateRenderer is a private method to Fyne which links this widget to it's renderer func (t *TextGrid) CreateRenderer() fyne.WidgetRenderer { t.ExtendBaseWidget(t) @@ -148,11 +221,13 @@ func (t *textGridRender) appendTextCell(str rune) { text := canvas.NewText(string(str), theme.TextColor()) text.TextStyle.Monospace = true - t.objects = append(t.objects, text) + bg := canvas.NewRectangle(color.Transparent) + t.objects = append(t.objects, bg, text) } -func (t *textGridRender) setCellRune(str rune, pos int, cellFG color.Color) { - text := t.objects[pos].(*canvas.Text) +func (t *textGridRender) setCellRune(str rune, pos int, style TextGridStyle) { + rect := t.objects[pos*2].(*canvas.Rectangle) + text := t.objects[pos*2+1].(*canvas.Text) if str == 0 { text.Text = " " } else { @@ -160,19 +235,24 @@ func (t *textGridRender) setCellRune(str rune, pos int, cellFG color.Color) { } fg := theme.TextColor() - if cellFG != nil { - fg = cellFG + if style != nil && style.TextColor() != nil { + fg = style.TextColor() } - text.Color = fg + + bg := color.Color(color.Transparent) + if style != nil && style.BackgroundColor() != nil { + bg = style.BackgroundColor() + } + rect.FillColor = bg } func (t *textGridRender) ensureGrid() { cellCount := t.cols * t.rows - if len(t.objects) == cellCount { + if len(t.objects) == cellCount*2 { return } - for i := len(t.objects); i < cellCount; i++ { + for i := len(t.objects); i < cellCount*2; i += 2 { t.appendTextCell(' ') } } @@ -189,16 +269,16 @@ func (t *textGridRender) refreshGrid() { if t.text.LineNumbers { lineStr := []rune(fmt.Sprintf("%d", line)) for c := 0; c < len(lineStr); c++ { - t.setCellRune(lineStr[c], x, whitespaceColor) // line numbers + t.setCellRune(lineStr[c], x, TextGridStyleWhitespace) // line numbers i++ x++ } for ; i < t.lineCountWidth(); i++ { - t.setCellRune(' ', x, whitespaceColor) // padding space + t.setCellRune(' ', x, TextGridStyleWhitespace) // padding space x++ } - t.setCellRune('|', x, whitespaceColor) // last space + t.setCellRune('|', x, TextGridStyleWhitespace) // last space i++ x++ } @@ -207,27 +287,33 @@ func (t *textGridRender) refreshGrid() { continue } if t.text.Whitespace && r.Rune == ' ' { - t.setCellRune(textAreaSpaceSymbol, x, whitespaceColor) // whitespace char + if r.Style != nil && r.Style.BackgroundColor() != nil { + whitespaceBG := &CustomTextGridStyle{FGColor: TextGridStyleWhitespace.TextColor(), + BGColor: r.Style.BackgroundColor()} + t.setCellRune(textAreaSpaceSymbol, x, whitespaceBG) // whitespace char + } else { + t.setCellRune(textAreaSpaceSymbol, x, TextGridStyleWhitespace) // whitespace char + } } else { - t.setCellRune(r.Rune, x, r.TextColor) // regular char + t.setCellRune(r.Rune, x, r.Style) // regular char } i++ x++ } if t.text.Whitespace && i < t.cols && rowIndex < len(t.text.Content)-1 { - t.setCellRune(textAreaNewLineSymbol, x, whitespaceColor) // newline + t.setCellRune(textAreaNewLineSymbol, x, TextGridStyleWhitespace) // newline i++ x++ } for ; i < t.cols; i++ { - t.setCellRune(' ', x, nil) // blanks + t.setCellRune(' ', x, TextGridStyleDefault) // blanks x++ } line++ } - for ; x < len(t.objects); x++ { - t.setCellRune(' ', x, nil) // blank lines? + for ; x < len(t.objects)/2; x++ { + t.setCellRune(' ', x, TextGridStyleDefault) // blank lines? } canvas.Refresh(t.text) } @@ -264,8 +350,10 @@ func (t *textGridRender) Layout(size fyne.Size) { cellPos := fyne.NewPos(0, 0) for y := 0; y < t.rows; y++ { for x := 0; x < t.cols; x++ { - t.objects[i].Move(cellPos) + t.objects[i*2+1].Move(cellPos) + t.objects[i*2].Resize(t.cellSize) + t.objects[i*2].Move(cellPos) cellPos.X += t.cellSize.Width i++ } diff --git a/widget/textgrid_test.go b/widget/textgrid_test.go index 90645591d8..20afd16214 100644 --- a/widget/textgrid_test.go +++ b/widget/textgrid_test.go @@ -36,13 +36,34 @@ func TestTextGrid_Rows(t *testing.T) { assert.Equal(t, 2, len(grid.Content[0])) } +func TestTextGrid_SetStyle(t *testing.T) { + grid := NewTextGridFromString("Abc") + grid.SetStyle(0, 1, &CustomTextGridStyle{FGColor: color.White, BGColor: color.Black}) + + assert.Nil(t, grid.Content[0][0].Style) + assert.Equal(t, color.White, grid.Content[0][1].Style.TextColor()) + assert.Equal(t, color.Black, grid.Content[0][1].Style.BackgroundColor()) +} + +func TestTextGrid_SetStyleRange(t *testing.T) { + grid := NewTextGridFromString("Ab\ncd") + grid.SetStyleRange(0, 1, 1, 0, &CustomTextGridStyle{FGColor: color.White, BGColor: color.Black}) + + assert.Nil(t, grid.Content[0][0].Style) + assert.Equal(t, color.White, grid.Content[0][1].Style.TextColor()) + assert.Equal(t, color.Black, grid.Content[0][1].Style.BackgroundColor()) + assert.Equal(t, color.White, grid.Content[1][0].Style.TextColor()) + assert.Equal(t, color.Black, grid.Content[1][0].Style.BackgroundColor()) + assert.Nil(t, grid.Content[1][1].Style) +} + func TestTextGrid_CreateRendererRows(t *testing.T) { grid := NewTextGrid() grid.Resize(fyne.NewSize(56, 22)) rend := test.WidgetRenderer(grid).(*textGridRender) rend.Refresh() - assert.Equal(t, 4, len(rend.objects)) + assert.Equal(t, 8, len(rend.objects)) } func TestTextGridRender_Size(t *testing.T) { @@ -62,21 +83,22 @@ func TestTextGridRender_Whitespace(t *testing.T) { 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 + // indexes of text are at n*2+1 due to bg rects appearing before letter objects + assert.Equal(t, string(textAreaSpaceSymbol), rend.objects[3].(*canvas.Text).Text) // col 1 is space + assert.Equal(t, string(textAreaNewLineSymbol), rend.objects[7].(*canvas.Text).Text) // col 3 is newline + assert.NotEqual(t, string(textAreaNewLineSymbol), rend.objects[11].(*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.Content[0][1].Style = &CustomTextGridStyle{FGColor: 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, 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) + assert.Equal(t, theme.TextColor(), rend.objects[1].(*canvas.Text).Color) + assert.Equal(t, color.Black, rend.objects[3].(*canvas.Text).Color) + assert.Equal(t, TextGridStyleWhitespace.TextColor(), rend.objects[5].(*canvas.Text).Color) }