Skip to content

Commit

Permalink
Add word selection on double-tap (#96)
Browse files Browse the repository at this point in the history
Implement word selection functionality triggered by a double-tap.
Words are identified as sequences of alphanumeric characters, while
spaces and punctuation are ignored. Update the selection logic to
highlight the word under the cursor and clear any existing selection.
Adjust tests to validate word selection and ensure accurate behavior
when tapping on non-alphanumeric characters.
  • Loading branch information
mgazza authored Oct 28, 2024
1 parent bc7d6e3 commit 02dbb6b
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 1 deletion.
8 changes: 8 additions & 0 deletions position.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,11 @@ func (t *Terminal) getTermPosition(pos fyne.Position) position {
row := int(pos.Y/cell.Height) + 1
return position{col, row}
}

// getTextPosition converts a terminal position (row and col) to fyne coordinates.
func (t *Terminal) getTextPosition(pos position) fyne.Position {
cell := t.guessCellSize()
x := (pos.Col - 1) * int(cell.Width) // Convert column to pixel position (1-based to 0-based)
y := (pos.Row - 1) * int(cell.Height) // Convert row to pixel position (1-based to 0-based)
return fyne.NewPos(float32(x), float32(y))
}
2 changes: 2 additions & 0 deletions select.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ func (t *Terminal) clearSelectedText() {
t.Refresh()
t.blockMode = false
t.selecting = false
t.selStart = nil
t.selEnd = nil
}

// SelectedText gets the text that is currently selected.
Expand Down
70 changes: 69 additions & 1 deletion select_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package terminal
import (
"testing"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
widget2 "github.com/fyne-io/terminal/internal/widget"
"github.com/stretchr/testify/assert"
)

func TestGetSelectedRange(t *testing.T) {
Expand Down Expand Up @@ -75,7 +77,6 @@ func TestGetSelectedRange(t *testing.T) {
}

func TestGetTextRange(t *testing.T) {
// Prepare the text grid for the tests
grid := widget2.NewTermGrid()
grid.Rows = []widget.TextGridRow{
{Cells: []widget.TextGridCell{{Rune: 'A'}, {Rune: 'B'}, {Rune: 'C'}}},
Expand Down Expand Up @@ -109,3 +110,70 @@ func TestGetTextRange(t *testing.T) {
})
}
}

func TestDoubleTapped(t *testing.T) {
grid := widget2.NewTermGrid()
grid.Rows = []widget.TextGridRow{
{Cells: []widget.TextGridCell{
{Rune: 'H'}, {Rune: 'e'}, {Rune: 'l'}, {Rune: 'l'}, {Rune: 'o'},
{Rune: ' '}, {Rune: 'W'}, {Rune: 'o'}, {Rune: 'r'}, {Rune: 'l'},
{Rune: 'd'}, {Rune: '!'},
}},
{Cells: []widget.TextGridCell{
{Rune: 'T'}, {Rune: 'e'}, {Rune: 's'}, {Rune: 't'}, {Rune: 'i'},
{Rune: 'n'}, {Rune: 'g'}, {Rune: ' '}, {Rune: '1'}, {Rune: '2'},
{Rune: '3'}, {Rune: '.'},
}},
}

term := &Terminal{
content: grid,
}
term.Resize(fyne.NewSize(500, 500))

tests := map[string]struct {
clickPosition fyne.Position
expectedWord string
}{
"Double tap on 'Hello'": {
clickPosition: term.getTextPosition(position{Row: 1, Col: 1}),
expectedWord: "Hello",
},
"Double tap on 'World'": {
clickPosition: term.getTextPosition(position{Row: 1, Col: 7}),
expectedWord: "World",
},
"Double tap on '123'": {
clickPosition: term.getTextPosition(position{Row: 2, Col: 9}),
expectedWord: "123",
},
"Double tap on '!' should not select": {
clickPosition: term.getTextPosition(position{Row: 1, Col: 12}),
expectedWord: "",
},
"Double tap on '.' should not select": {
clickPosition: term.getTextPosition(position{Row: 2, Col: 12}),
expectedWord: "",
},
"Double tap on space between words": {
clickPosition: term.getTextPosition(position{Row: 1, Col: 6}),
expectedWord: "",
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
term.clearSelectedText()
term.DoubleTapped(&fyne.PointEvent{
Position: tc.clickPosition,
})

selectedWord := ""
if term.hasSelectedText() {
selectedWord = term.SelectedText()
}

assert.Equal(t, tc.expectedWord, selectedWord)
})
}
}
46 changes: 46 additions & 0 deletions term.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"runtime"
"sync"
"time"
"unicode"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
Expand Down Expand Up @@ -158,6 +159,51 @@ func (t *Terminal) MouseUp(ev *desktop.MouseEvent) {
}
}

// DoubleTapped handles the double tapped event.
func (t *Terminal) DoubleTapped(pe *fyne.PointEvent) {
pos := t.sanitizePosition(pe.Position)
termPos := t.getTermPosition(*pos)
row, col := termPos.Row, termPos.Col

if t.hasSelectedText() {
t.clearSelectedText()
}

if row < 1 || row > len(t.content.Rows) {
return
}

rowContent := t.content.Rows[row-1].Cells

if col < 0 || col >= len(rowContent) {
return // No valid character under the cursor, do nothing
}

start, end := col-1, col-1

if !unicode.IsLetter(rowContent[start].Rune) && !unicode.IsDigit(rowContent[start].Rune) {
return
}

for start > 0 && (unicode.IsLetter(rowContent[start-1].Rune) || unicode.IsDigit(rowContent[start-1].Rune)) {
start--
}
if start < len(rowContent) && !unicode.IsLetter(rowContent[start].Rune) && !unicode.IsDigit(rowContent[start].Rune) {
start++
}
for end < len(rowContent) && (unicode.IsLetter(rowContent[end].Rune) || unicode.IsDigit(rowContent[end].Rune)) {
end++
}
if start == end {
return
}

t.selStart = &position{Row: row, Col: start + 1}
t.selEnd = &position{Row: row, Col: end}

t.highlightSelectedText()
}

// RemoveListener de-registers a Config channel and closes it
func (t *Terminal) RemoveListener(listener chan Config) {
t.listenerLock.Lock()
Expand Down

0 comments on commit 02dbb6b

Please sign in to comment.