mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-07 02:15:34 +00:00
widget: add optional password masking to Editor
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>
This commit is contained in:
+61
-1
@@ -4,9 +4,11 @@ package widget
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
"gioui.org/gesture"
|
"gioui.org/gesture"
|
||||||
@@ -31,6 +33,10 @@ type Editor struct {
|
|||||||
// Submit enabled translation of carriage return keys to SubmitEvents.
|
// Submit enabled translation of carriage return keys to SubmitEvents.
|
||||||
// If not enabled, carriage returns are inserted as newlines in the text.
|
// If not enabled, carriage returns are inserted as newlines in the text.
|
||||||
Submit bool
|
Submit bool
|
||||||
|
// Mask replaces the visual display of each rune in the contents with the given rune.
|
||||||
|
// Newline characters are not masked. When non-zero, the unmasked contents
|
||||||
|
// are accessed by Len, Text, and SetText.
|
||||||
|
Mask rune
|
||||||
|
|
||||||
eventKey int
|
eventKey int
|
||||||
font text.Font
|
font text.Font
|
||||||
@@ -39,6 +45,8 @@ type Editor struct {
|
|||||||
blinkStart time.Time
|
blinkStart time.Time
|
||||||
focused bool
|
focused bool
|
||||||
rr editBuffer
|
rr editBuffer
|
||||||
|
maskReader maskReader
|
||||||
|
lastMask rune
|
||||||
maxWidth int
|
maxWidth int
|
||||||
viewSize image.Point
|
viewSize image.Point
|
||||||
valid bool
|
valid bool
|
||||||
@@ -75,6 +83,49 @@ type Editor struct {
|
|||||||
prevEvents int
|
prevEvents int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type maskReader struct {
|
||||||
|
// rr is the underlying reader.
|
||||||
|
rr io.RuneReader
|
||||||
|
maskBuf [utf8.UTFMax]byte
|
||||||
|
// mask is the utf-8 encoded mask rune.
|
||||||
|
mask []byte
|
||||||
|
// overflow contains excess mask bytes left over after the last Read call.
|
||||||
|
overflow []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *maskReader) Reset(r io.RuneReader, mr rune) {
|
||||||
|
m.rr = r
|
||||||
|
n := utf8.EncodeRune(m.maskBuf[:], mr)
|
||||||
|
m.mask = m.maskBuf[:n]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads from the underlying reader and replaces every
|
||||||
|
// rune with the mask rune.
|
||||||
|
func (m *maskReader) Read(b []byte) (n int, err error) {
|
||||||
|
for len(b) > 0 {
|
||||||
|
var replacement []byte
|
||||||
|
if len(m.overflow) > 0 {
|
||||||
|
replacement = m.overflow
|
||||||
|
} else {
|
||||||
|
var r rune
|
||||||
|
r, _, err = m.rr.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if r == '\n' {
|
||||||
|
replacement = []byte{'\n'}
|
||||||
|
} else {
|
||||||
|
replacement = m.mask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nn := copy(b, replacement)
|
||||||
|
m.overflow = replacement[nn:]
|
||||||
|
n += nn
|
||||||
|
b = b[nn:]
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
type EditorEvent interface {
|
type EditorEvent interface {
|
||||||
isEditorEvent()
|
isEditorEvent()
|
||||||
}
|
}
|
||||||
@@ -272,6 +323,10 @@ func (e *Editor) Layout(gtx layout.Context, sh text.Shaper, font text.Font, size
|
|||||||
e.shaper = sh
|
e.shaper = sh
|
||||||
e.invalidate()
|
e.invalidate()
|
||||||
}
|
}
|
||||||
|
if e.Mask != e.lastMask {
|
||||||
|
e.lastMask = e.Mask
|
||||||
|
e.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
e.makeValid()
|
e.makeValid()
|
||||||
e.processEvents(gtx)
|
e.processEvents(gtx)
|
||||||
@@ -470,7 +525,12 @@ func (e *Editor) moveCoord(pos image.Point) {
|
|||||||
|
|
||||||
func (e *Editor) layoutText(s text.Shaper) ([]text.Line, layout.Dimensions) {
|
func (e *Editor) layoutText(s text.Shaper) ([]text.Line, layout.Dimensions) {
|
||||||
e.rr.Reset()
|
e.rr.Reset()
|
||||||
lines, _ := s.Layout(e.font, e.textSize, e.maxWidth, &e.rr)
|
var r io.Reader = &e.rr
|
||||||
|
if e.Mask != 0 {
|
||||||
|
e.maskReader.Reset(&e.rr, e.Mask)
|
||||||
|
r = &e.maskReader
|
||||||
|
}
|
||||||
|
lines, _ := s.Layout(e.font, e.textSize, e.maxWidth, r)
|
||||||
dims := linesDimens(lines)
|
dims := linesDimens(lines)
|
||||||
for i := 0; i < len(lines)-1; i++ {
|
for i := 0; i < len(lines)-1; i++ {
|
||||||
// To avoid layout flickering while editing, assume a soft newline takes
|
// To avoid layout flickering while editing, assume a soft newline takes
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"testing/quick"
|
"testing/quick"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
"gioui.org/font/gofont"
|
"gioui.org/font/gofont"
|
||||||
@@ -43,6 +44,28 @@ func TestEditor(t *testing.T) {
|
|||||||
assertCaret(t, e, 1, 4, len("æbc\naøå•"))
|
assertCaret(t, e, 1, 4, len("æbc\naøå•"))
|
||||||
e.Move(+1)
|
e.Move(+1)
|
||||||
assertCaret(t, e, 1, 4, len("æbc\naøå•"))
|
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
|
// assertCaret asserts that the editor caret is at a particular line
|
||||||
|
|||||||
Reference in New Issue
Block a user