mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 15:45:38 +00:00
5c0f190849
This change adds optional password masking to the Editor. To enable this feature, set the new Mask field to a non-zero rune. Every rune in the Editor's contents will be replaced by the mask rune in the visual display, except for newlines. The actual contents of the editor can still be accessed with Len, Text, and SetText. Fixes gio#80 Signed-off-by: tainted-bit <sourcehut@taintedbit.com>
163 lines
4.0 KiB
Go
163 lines
4.0 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package widget
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"math/rand"
|
|
"reflect"
|
|
"testing"
|
|
"testing/quick"
|
|
"unicode"
|
|
|
|
"gioui.org/f32"
|
|
"gioui.org/font/gofont"
|
|
"gioui.org/layout"
|
|
"gioui.org/op"
|
|
"gioui.org/text"
|
|
"gioui.org/unit"
|
|
)
|
|
|
|
func TestEditor(t *testing.T) {
|
|
e := new(Editor)
|
|
gtx := layout.Context{
|
|
Ops: new(op.Ops),
|
|
Constraints: layout.Exact(image.Pt(100, 100)),
|
|
}
|
|
cache := text.NewCache(gofont.Collection())
|
|
fontSize := unit.Px(10)
|
|
font := text.Font{}
|
|
|
|
e.SetText("æbc\naøå•")
|
|
e.Layout(gtx, cache, font, fontSize)
|
|
assertCaret(t, e, 0, 0, 0)
|
|
e.moveEnd()
|
|
assertCaret(t, e, 0, 3, len("æbc"))
|
|
e.Move(+1)
|
|
assertCaret(t, e, 1, 0, len("æbc\n"))
|
|
e.Move(-1)
|
|
assertCaret(t, e, 0, 3, len("æbc"))
|
|
e.moveLines(+1)
|
|
assertCaret(t, e, 1, 3, len("æbc\naøå"))
|
|
e.moveEnd()
|
|
assertCaret(t, e, 1, 4, len("æbc\naøå•"))
|
|
e.Move(+1)
|
|
assertCaret(t, e, 1, 4, len("æbc\naøå•"))
|
|
|
|
// Ensure that password masking does not affect caret behavior
|
|
e.Move(-3)
|
|
assertCaret(t, e, 1, 1, len("æbc\na"))
|
|
e.Mask = '*'
|
|
e.Layout(gtx, cache, font, fontSize)
|
|
assertCaret(t, e, 1, 1, len("æbc\na"))
|
|
e.Move(-3)
|
|
assertCaret(t, e, 0, 2, len("æb"))
|
|
e.Mask = '\U0001F92B'
|
|
e.Layout(gtx, cache, font, fontSize)
|
|
e.moveEnd()
|
|
assertCaret(t, e, 0, 3, len("æbc"))
|
|
|
|
// When a password mask is applied, it should replace all visible glyphs
|
|
for i, line := range e.lines {
|
|
for j, glyph := range line.Layout {
|
|
if glyph.Rune != e.Mask && !unicode.IsSpace(glyph.Rune) {
|
|
t.Errorf("glyph at (%d, %d) is unmasked rune %d", i, j, glyph.Rune)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// assertCaret asserts that the editor caret is at a particular line
|
|
// and column, and that the byte position matches as well.
|
|
func assertCaret(t *testing.T, e *Editor, line, col, bytes int) {
|
|
t.Helper()
|
|
gotLine, gotCol := e.CaretPos()
|
|
if gotLine != line || gotCol != col {
|
|
t.Errorf("caret at (%d, %d), expected (%d, %d)", gotLine, gotCol, line, col)
|
|
}
|
|
if bytes != e.rr.caret {
|
|
t.Errorf("caret at buffer position %d, expected %d", e.rr.caret, bytes)
|
|
}
|
|
}
|
|
|
|
type editMutation int
|
|
|
|
const (
|
|
setText editMutation = iota
|
|
moveRune
|
|
moveLine
|
|
movePage
|
|
moveStart
|
|
moveEnd
|
|
moveCoord
|
|
moveLast // Mark end; never generated.
|
|
)
|
|
|
|
func TestEditorCaretConsistency(t *testing.T) {
|
|
gtx := layout.Context{
|
|
Ops: new(op.Ops),
|
|
Constraints: layout.Exact(image.Pt(100, 100)),
|
|
}
|
|
cache := text.NewCache(gofont.Collection())
|
|
fontSize := unit.Px(10)
|
|
font := text.Font{}
|
|
for _, a := range []text.Alignment{text.Start, text.Middle, text.End} {
|
|
e := &Editor{
|
|
Alignment: a,
|
|
}
|
|
e.Layout(gtx, cache, font, fontSize)
|
|
|
|
consistent := func() error {
|
|
t.Helper()
|
|
gotLine, gotCol := e.CaretPos()
|
|
gotCoords := e.CaretCoords()
|
|
wantLine, wantCol, wantX, wantY := e.layoutCaret()
|
|
wantCoords := f32.Pt(float32(wantX)/64, float32(wantY))
|
|
if wantLine == gotLine && wantCol == gotCol && gotCoords == wantCoords {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("caret (%d,%d) pos %s, want (%d,%d) pos %s", gotLine, gotCol, gotCoords, wantLine, wantCol, wantCoords)
|
|
}
|
|
if err := consistent(); err != nil {
|
|
t.Errorf("initial editor inconsistency (alignment %s): %v", a, err)
|
|
}
|
|
|
|
move := func(mutation editMutation, str string, distance int8, x, y uint16) bool {
|
|
switch mutation {
|
|
case setText:
|
|
e.SetText(str)
|
|
e.Layout(gtx, cache, font, fontSize)
|
|
case moveRune:
|
|
e.Move(int(distance))
|
|
case moveLine:
|
|
e.moveLines(int(distance))
|
|
case movePage:
|
|
e.movePages(int(distance))
|
|
case moveStart:
|
|
e.moveStart()
|
|
case moveEnd:
|
|
e.moveEnd()
|
|
case moveCoord:
|
|
e.moveCoord(image.Pt(int(x), int(y)))
|
|
default:
|
|
return false
|
|
}
|
|
if err := consistent(); err != nil {
|
|
t.Error(err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
if err := quick.Check(move, nil); err != nil {
|
|
t.Errorf("editor inconsistency (alignment %s): %v", a, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate generates a value of itself, for testing/quick.
|
|
func (editMutation) Generate(rand *rand.Rand, size int) reflect.Value {
|
|
t := editMutation(rand.Intn(int(moveLast)))
|
|
return reflect.ValueOf(t)
|
|
}
|