Skip to content

Commit b632e74

Browse files
committed
Add blinking
Add the ability to process blinking text
1 parent 41c4a47 commit b632e74

File tree

8 files changed

+367
-23
lines changed

8 files changed

+367
-23
lines changed

color.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func (t *Terminal) handleColorEscape(message string) {
4646
t.currentBG = nil
4747
t.currentFG = nil
4848
t.bold = false
49+
t.blinking = false
4950
return
5051
}
5152
modes := strings.Split(message, ";")
@@ -80,9 +81,12 @@ func (t *Terminal) handleColorMode(modeStr string) {
8081
case 0:
8182
t.currentBG, t.currentFG = nil, nil
8283
t.bold = false
84+
t.blinking = false
8385
case 1:
8486
t.bold = true
8587
case 4, 24: //italic
88+
case 5:
89+
t.blinking = true
8690
case 7: // reverse
8791
bg, fg := t.currentBG, t.currentFG
8892
if fg == nil {

color_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"fyne.io/fyne/v2"
1010
"fyne.io/fyne/v2/widget"
11+
widget2 "github.com/fyne-io/terminal/internal/widget"
1112
"github.com/stretchr/testify/assert"
1213
)
1314

@@ -913,7 +914,7 @@ func TestHandleOutput_BufferCutoff(t *testing.T) {
913914
term.Resize(termsize)
914915
term.handleOutput([]byte("\x1b[38;5;64"))
915916
term.handleOutput([]byte("m40\x1b[38;5;65m41"))
916-
tg := widget.NewTextGrid()
917+
tg := widget2.NewTermGrid()
917918
tg.Resize(termsize)
918919
c1 := &color.RGBA{R: 95, G: 135, A: 255}
919920
c2 := &color.RGBA{R: 95, G: 135, B: 95, A: 255}

internal/widget/termgrid.go

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
package widget
2+
3+
import (
4+
"context"
5+
"image/color"
6+
"math"
7+
"strconv"
8+
"time"
9+
10+
"fyne.io/fyne/v2/widget"
11+
12+
"fyne.io/fyne/v2"
13+
"fyne.io/fyne/v2/canvas"
14+
"fyne.io/fyne/v2/theme"
15+
)
16+
17+
const (
18+
textAreaSpaceSymbol = '·'
19+
textAreaTabSymbol = '→'
20+
textAreaNewLineSymbol = '↵'
21+
blinkingInterval = 500 * time.Millisecond
22+
)
23+
24+
// TermGrid is a monospaced grid of characters.
25+
// This is designed to be used by our terminal emulator.
26+
type TermGrid struct {
27+
widget.TextGrid
28+
}
29+
30+
// CreateRenderer is a private method to Fyne which links this widget to it's renderer
31+
func (t *TermGrid) CreateRenderer() fyne.WidgetRenderer {
32+
t.ExtendBaseWidget(t)
33+
render := &termGridRenderer{text: t}
34+
render.updateCellSize()
35+
// N.B these global variables are not a good idea.
36+
widget.TextGridStyleDefault = &widget.CustomTextGridStyle{}
37+
widget.TextGridStyleWhitespace = &widget.CustomTextGridStyle{FGColor: theme.DisabledColor()}
38+
39+
return render
40+
}
41+
42+
// NewTermGrid creates a new empty TextGrid widget.
43+
func NewTermGrid() *TermGrid {
44+
grid := &TermGrid{}
45+
grid.ExtendBaseWidget(grid)
46+
return grid
47+
}
48+
49+
type termGridRenderer struct {
50+
text *TermGrid
51+
52+
cols, rows int
53+
54+
cellSize fyne.Size
55+
objects []fyne.CanvasObject
56+
current fyne.Canvas
57+
blink bool
58+
shouldBlink bool
59+
tickerCancel context.CancelFunc
60+
}
61+
62+
func (t *termGridRenderer) appendTextCell(str rune) {
63+
text := canvas.NewText(string(str), theme.ForegroundColor())
64+
text.TextStyle.Monospace = true
65+
66+
bg := canvas.NewRectangle(color.Transparent)
67+
t.objects = append(t.objects, bg, text)
68+
}
69+
70+
func (t *termGridRenderer) refreshCell(row, col int) {
71+
pos := row*t.cols + col
72+
if pos*2+1 >= len(t.objects) {
73+
return
74+
}
75+
76+
cell := t.text.Rows[row].Cells[col]
77+
t.setCellRune(cell.Rune, pos, cell.Style)
78+
}
79+
80+
func (t *termGridRenderer) setCellRune(str rune, pos int, style widget.TextGridStyle) {
81+
if str == 0 {
82+
str = ' '
83+
}
84+
fg := theme.ForegroundColor()
85+
if style != nil && style.TextColor() != nil {
86+
fg = style.TextColor()
87+
}
88+
bg := color.Color(color.Transparent)
89+
if style != nil && style.BackgroundColor() != nil {
90+
bg = style.BackgroundColor()
91+
}
92+
93+
if s, ok := style.(*TermTextGridStyle); ok && s != nil && s.BlinkEnabled {
94+
t.shouldBlink = true
95+
if t.blink {
96+
fg = bg
97+
}
98+
}
99+
100+
text := t.objects[pos*2+1].(*canvas.Text)
101+
text.TextSize = theme.TextSize()
102+
103+
newStr := string(str)
104+
if text.Text != newStr || text.Color != fg {
105+
text.Text = newStr
106+
text.Color = fg
107+
t.refresh(text)
108+
}
109+
110+
rect := t.objects[pos*2].(*canvas.Rectangle)
111+
if rect.FillColor != bg {
112+
rect.FillColor = bg
113+
t.refresh(rect)
114+
}
115+
}
116+
117+
func (t *termGridRenderer) addCellsIfRequired() {
118+
cellCount := t.cols * t.rows
119+
if len(t.objects) == cellCount*2 {
120+
return
121+
}
122+
for i := len(t.objects); i < cellCount*2; i += 2 {
123+
t.appendTextCell(' ')
124+
}
125+
}
126+
127+
func (t *termGridRenderer) refreshGrid() {
128+
line := 1
129+
x := 0
130+
// reset shouldBlink which can be set by setCellRune if a cell with BlinkEnabled is found
131+
t.shouldBlink = false
132+
133+
for rowIndex, row := range t.text.Rows {
134+
i := 0
135+
if t.text.ShowLineNumbers {
136+
lineStr := []rune(strconv.Itoa(line))
137+
pad := t.lineNumberWidth() - len(lineStr)
138+
for ; i < pad; i++ {
139+
t.setCellRune(' ', x, widget.TextGridStyleWhitespace) // padding space
140+
x++
141+
}
142+
for c := 0; c < len(lineStr); c++ {
143+
t.setCellRune(lineStr[c], x, widget.TextGridStyleDefault) // line numbers
144+
i++
145+
x++
146+
}
147+
148+
t.setCellRune('|', x, widget.TextGridStyleWhitespace) // last space
149+
i++
150+
x++
151+
}
152+
for _, r := range row.Cells {
153+
if i >= t.cols { // would be an overflow - bad
154+
continue
155+
}
156+
if t.text.ShowWhitespace && (r.Rune == ' ' || r.Rune == '\t') {
157+
sym := textAreaSpaceSymbol
158+
if r.Rune == '\t' {
159+
sym = textAreaTabSymbol
160+
}
161+
162+
if r.Style != nil && r.Style.BackgroundColor() != nil {
163+
whitespaceBG := &widget.CustomTextGridStyle{FGColor: widget.TextGridStyleWhitespace.TextColor(),
164+
BGColor: r.Style.BackgroundColor()}
165+
t.setCellRune(sym, x, whitespaceBG) // whitespace char
166+
} else {
167+
t.setCellRune(sym, x, widget.TextGridStyleWhitespace) // whitespace char
168+
}
169+
} else {
170+
t.setCellRune(r.Rune, x, r.Style) // regular char
171+
}
172+
i++
173+
x++
174+
}
175+
if t.text.ShowWhitespace && i < t.cols && rowIndex < len(t.text.Rows)-1 {
176+
t.setCellRune(textAreaNewLineSymbol, x, widget.TextGridStyleWhitespace) // newline
177+
i++
178+
x++
179+
}
180+
for ; i < t.cols; i++ {
181+
t.setCellRune(' ', x, widget.TextGridStyleDefault) // blanks
182+
x++
183+
}
184+
185+
line++
186+
}
187+
for ; x < len(t.objects)/2; x++ {
188+
t.setCellRune(' ', x, widget.TextGridStyleDefault) // trailing cells and blank lines
189+
}
190+
191+
switch {
192+
case t.shouldBlink && t.tickerCancel == nil:
193+
t.runBlink()
194+
case !t.shouldBlink && t.tickerCancel != nil:
195+
t.tickerCancel()
196+
t.tickerCancel = nil
197+
}
198+
}
199+
200+
func (t *termGridRenderer) runBlink() {
201+
if t.tickerCancel != nil {
202+
t.tickerCancel()
203+
t.tickerCancel = nil
204+
}
205+
var tickerContext context.Context
206+
tickerContext, t.tickerCancel = context.WithCancel(context.Background())
207+
ticker := time.NewTicker(blinkingInterval)
208+
blinking := false
209+
go func() {
210+
for {
211+
select {
212+
case <-tickerContext.Done():
213+
return
214+
case <-ticker.C:
215+
t.SetBlink(blinking)
216+
blinking = !blinking
217+
t.refreshGrid()
218+
}
219+
}
220+
}()
221+
}
222+
223+
func (t *termGridRenderer) lineNumberWidth() int {
224+
return len(strconv.Itoa(t.rows + 1))
225+
}
226+
227+
func (t *termGridRenderer) updateGridSize(size fyne.Size) {
228+
bufRows := len(t.text.Rows)
229+
bufCols := 0
230+
for _, row := range t.text.Rows {
231+
bufCols = int(math.Max(float64(bufCols), float64(len(row.Cells))))
232+
}
233+
sizeCols := math.Floor(float64(size.Width) / float64(t.cellSize.Width))
234+
sizeRows := math.Floor(float64(size.Height) / float64(t.cellSize.Height))
235+
236+
if t.text.ShowWhitespace {
237+
bufCols++
238+
}
239+
if t.text.ShowLineNumbers {
240+
bufCols += t.lineNumberWidth()
241+
}
242+
243+
t.cols = int(math.Max(sizeCols, float64(bufCols)))
244+
t.rows = int(math.Max(sizeRows, float64(bufRows)))
245+
t.addCellsIfRequired()
246+
}
247+
248+
func (t *termGridRenderer) Layout(size fyne.Size) {
249+
t.updateGridSize(size)
250+
251+
i := 0
252+
cellPos := fyne.NewPos(0, 0)
253+
for y := 0; y < t.rows; y++ {
254+
for x := 0; x < t.cols; x++ {
255+
t.objects[i*2+1].Move(cellPos)
256+
257+
t.objects[i*2].Resize(t.cellSize)
258+
t.objects[i*2].Move(cellPos)
259+
cellPos.X += t.cellSize.Width
260+
i++
261+
}
262+
263+
cellPos.X = 0
264+
cellPos.Y += t.cellSize.Height
265+
}
266+
}
267+
268+
func (t *termGridRenderer) MinSize() fyne.Size {
269+
longestRow := float32(0)
270+
for _, row := range t.text.Rows {
271+
longestRow = fyne.Max(longestRow, float32(len(row.Cells)))
272+
}
273+
return fyne.NewSize(t.cellSize.Width*longestRow,
274+
t.cellSize.Height*float32(len(t.text.Rows)))
275+
}
276+
277+
func (t *termGridRenderer) Refresh() {
278+
// we may be on a new canvas, so just update it to be sure
279+
if fyne.CurrentApp() != nil && fyne.CurrentApp().Driver() != nil {
280+
t.current = fyne.CurrentApp().Driver().CanvasForObject(t.text)
281+
}
282+
283+
// theme could change text size
284+
t.updateCellSize()
285+
286+
widget.TextGridStyleWhitespace = &widget.CustomTextGridStyle{FGColor: theme.DisabledColor()}
287+
t.updateGridSize(t.text.Size())
288+
t.refreshGrid()
289+
}
290+
291+
func (t *termGridRenderer) ApplyTheme() {
292+
}
293+
294+
func (t *termGridRenderer) Objects() []fyne.CanvasObject {
295+
return t.objects
296+
}
297+
298+
func (t *termGridRenderer) Destroy() {
299+
}
300+
301+
func (t *termGridRenderer) refresh(obj fyne.CanvasObject) {
302+
if t.current == nil {
303+
if fyne.CurrentApp() != nil && fyne.CurrentApp().Driver() != nil {
304+
// cache canvas for this widget, so we don't look it up many times for every cell/row refresh!
305+
t.current = fyne.CurrentApp().Driver().CanvasForObject(t.text)
306+
}
307+
308+
if t.current == nil {
309+
return // not yet set up perhaps?
310+
}
311+
}
312+
313+
t.current.Refresh(obj)
314+
}
315+
316+
func (t *termGridRenderer) updateCellSize() {
317+
size := fyne.MeasureText("M", theme.TextSize(), fyne.TextStyle{Monospace: true})
318+
319+
// round it for seamless background
320+
size.Width = float32(math.Round(float64((size.Width))))
321+
size.Height = float32(math.Round(float64((size.Height))))
322+
323+
t.cellSize = size
324+
}
325+
326+
func (t *termGridRenderer) SetBlink(b bool) {
327+
t.blink = b
328+
}
329+
330+
type BlinkingRender interface {
331+
SetBlink(b bool)
332+
}

0 commit comments

Comments
 (0)