// TODO: this needs to be updated // SPDX-License-Identifier: Unlicense OR MIT package gui import ( "fmt" "image" "math/rand" "reflect" "testing" "testing/quick" "unicode" "git.mleku.dev/mleku/prevara/gio/f32" "git.mleku.dev/mleku/prevara/gio/font/gofont" "git.mleku.dev/mleku/prevara/gio/io/event" "git.mleku.dev/mleku/prevara/gio/io/key" "git.mleku.dev/mleku/prevara/gio/layout" "git.mleku.dev/mleku/prevara/gio/op" "git.mleku.dev/mleku/prevara/gio/text" "git.mleku.dev/mleku/prevara/gio/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) } } } } func TestEditorDimensions(t *testing.T) { e := new(Editor) tq := &testQueue{ events: []event.Event{ key.EditEvent{Text: "A"}, }, } gtx := layout.Context{ Ops: new(op.Ops), Constraints: layout.Constraints{Max: image.Pt(100, 100)}, Queue: tq, } cache := text.NewCache(gofont.Collection()) fontSize := unit.Px(10) font := text.Font{} dims := e.Layout(gtx, cache, font, fontSize) if dims.Size.X == 0 { t.Errorf("EditEvent was not reflected in _editor width") } } type testQueue struct { events []event.Event } func (q *testQueue) Events(_ event.Tag) []event.Event { return q.events } // 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 moveWord deleteWord 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() (e 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 e := consistent(); dbg.Chk(e) { 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))) case moveWord: e.moveWord(int(distance)) case deleteWord: e.deleteWord(int(distance)) default: return false } if e := consistent(); dbg.Chk(e) { t. return false } return true } if e := quick.Check(move, nil); dbg.Chk(e) { t.Errorf("editor inconsistency (alignment %s): %v", a, err) } } } func TestEditorMoveWord(t *testing.T) { type Test struct { Text string Start int Skip int Want int } tests := []Test{ {"", 0, 0, 0}, {"", 0, -1, 0}, {"", 0, 1, 0}, {"hello", 0, -1, 0}, {"hello", 0, 1, 5}, {"hello world", 3, 1, 5}, {"hello world", 3, -1, 0}, {"hello world", 8, -1, 6}, {"hello world", 8, 1, 11}, {"hello world", 3, 1, 5}, {"hello world", 3, 2, 14}, {"hello world", 8, 1, 14}, {"hello world", 8, -1, 0}, {"hello brave new world", 0, 3, 15}, } setup := func(t string) *Editor { 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(t) e.Layout(gtx, cache, font, fontSize) return e } for ii, tt := range tests { e := setup(tt.Text) e.Move(tt.Start) e.moveWord(tt.Skip) if e.rr.caret != tt.Want { t.Fatalf("[%d] moveWord: bad caret position: got %d, want %d", ii, e.rr.caret, tt.Want) } } } func TestEditorDeleteWord(t *testing.T) { type Test struct { Text string Start int Delete int Want int Result string } tests := []Test{ {"", 0, 0, 0, ""}, {"", 0, -1, 0, ""}, {"", 0, 1, 0, ""}, {"hello", 0, -1, 0, "hello"}, {"hello", 0, 1, 0, ""}, {"hello world", 3, 1, 3, "hel world"}, {"hello world", 3, -1, 0, "lo world"}, {"hello world", 8, -1, 6, "hello rld"}, {"hello world", 8, 1, 8, "hello wo"}, {"hello world", 3, 1, 3, "hel world"}, {"hello world", 3, 2, 3, "helworld"}, {"hello world", 8, 1, 8, "hello "}, {"hello world", 8, -1, 5, "hello world"}, {"hello brave new world", 0, 3, 0, " new world"}, } setup := func(t string) *Editor { 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(t) e.Layout(gtx, cache, font, fontSize) return e } for ii, tt := range tests { e := setup(tt.Text) e.Move(tt.Start) e.deleteWord(tt.Delete) if e.rr.caret != tt.Want { t.Fatalf("[%d] deleteWord: bad caret position: got %d, want %d", ii, e.rr.caret, tt.Want) } if e.Text() != tt.Result { t.Fatalf("[%d] deleteWord: invalid result: got %q, want %q", ii, e.Text(), tt.Result) } } } func TestEditorNoLayout(t *testing.T) { var e Editor e.SetText("hi!\n") e.Move(1) } // 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) }