gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/app/ime_test.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 //go:build go1.18 4 // +build go1.18 5 6 package app 7 8 import ( 9 "testing" 10 "unicode/utf8" 11 12 "gioui.org/font" 13 "gioui.org/font/gofont" 14 "gioui.org/io/input" 15 "gioui.org/io/key" 16 "gioui.org/layout" 17 "gioui.org/op" 18 "gioui.org/text" 19 "gioui.org/unit" 20 "gioui.org/widget" 21 ) 22 23 func FuzzIME(f *testing.F) { 24 runes := []rune("Hello, 世界! 🤬 علي،الحسنب北查爾斯頓工廠的安全漏洞已") 25 f.Add([]byte("20\x0010")) 26 f.Add([]byte("80000")) 27 f.Add([]byte("2008\"80\r00")) 28 f.Add([]byte("20007900002\x02000")) 29 f.Add([]byte("20007800002\x02000")) 30 f.Add([]byte("200A02000990\x19002\x17\x0200")) 31 f.Fuzz(func(t *testing.T, cmds []byte) { 32 cache := text.NewShaper(text.WithCollection(gofont.Collection())) 33 e := new(widget.Editor) 34 35 var r input.Router 36 gtx := layout.Context{Ops: new(op.Ops), Source: r.Source()} 37 gtx.Execute(key.FocusCmd{Tag: e}) 38 // Layout once to register focus. 39 e.Layout(gtx, cache, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{}) 40 r.Frame(gtx.Ops) 41 42 var state editorState 43 const ( 44 cmdReplace = iota 45 cmdSelect 46 cmdSnip 47 maxCmd 48 ) 49 const cmdLen = 5 50 for len(cmds) >= cmdLen { 51 n := e.Len() 52 rng := key.Range{ 53 Start: int(cmds[1]) % (n + 1), 54 End: int(cmds[2]) % (n + 1), 55 } 56 switch cmds[0] % cmdLen { 57 case cmdReplace: 58 rstart := int(cmds[3]) % len(runes) 59 rend := int(cmds[4]) % len(runes) 60 if rstart > rend { 61 rstart, rend = rend, rstart 62 } 63 replacement := string(runes[rstart:rend]) 64 state.Replace(rng, replacement) 65 r.Queue(key.EditEvent{Range: rng, Text: replacement}) 66 r.Queue(key.SnippetEvent(state.Snippet.Range)) 67 case cmdSelect: 68 r.Queue(key.SelectionEvent(rng)) 69 runes := []rune(e.Text()) 70 if rng.Start < 0 { 71 rng.Start = 0 72 } 73 if rng.End < 0 { 74 rng.End = 0 75 } 76 if rng.Start > len(runes) { 77 rng.Start = len(runes) 78 } 79 if rng.End > len(runes) { 80 rng.End = len(runes) 81 } 82 state.Selection.Range = rng 83 case cmdSnip: 84 r.Queue(key.SnippetEvent(rng)) 85 runes := []rune(e.Text()) 86 if rng.Start > rng.End { 87 rng.Start, rng.End = rng.End, rng.Start 88 } 89 if rng.Start < 0 { 90 rng.Start = 0 91 } 92 if rng.End < 0 { 93 rng.End = 0 94 } 95 if rng.Start > len(runes) { 96 rng.Start = len(runes) 97 } 98 if rng.End > len(runes) { 99 rng.End = len(runes) 100 } 101 state.Snippet = key.Snippet{ 102 Range: rng, 103 Text: string(runes[rng.Start:rng.End]), 104 } 105 } 106 cmds = cmds[cmdLen:] 107 e.Layout(gtx, cache, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{}) 108 r.Frame(gtx.Ops) 109 newState := r.EditorState() 110 // We don't track caret position. 111 state.Selection.Caret = newState.Selection.Caret 112 // Expanded snippets are ok. 113 their, our := newState.Snippet, state.EditorState.Snippet 114 beforeLen := 0 115 for before := our.Start - their.Start; before > 0; before-- { 116 _, n := utf8.DecodeRuneInString(their.Text[beforeLen:]) 117 beforeLen += n 118 } 119 afterLen := 0 120 for after := their.End - our.End; after > 0; after-- { 121 _, n := utf8.DecodeLastRuneInString(their.Text[:len(their.Text)-afterLen]) 122 afterLen += n 123 } 124 if beforeLen > 0 { 125 our.Text = their.Text[:beforeLen] + our.Text 126 our.Start = their.Start 127 } 128 if afterLen > 0 { 129 our.Text = our.Text + their.Text[len(their.Text)-afterLen:] 130 our.End = their.End 131 } 132 state.EditorState.Snippet = our 133 if newState != state.EditorState { 134 t.Errorf("IME state: %+v\neditor state: %+v", state.EditorState, newState) 135 } 136 } 137 }) 138 } 139 140 func TestEditorIndices(t *testing.T) { 141 var s editorState 142 const str = "Hello, 😀" 143 s.Snippet = key.Snippet{ 144 Text: str, 145 Range: key.Range{ 146 Start: 10, 147 End: utf8.RuneCountInString(str), 148 }, 149 } 150 utf16Indices := [...]struct { 151 Runes, UTF16 int 152 }{ 153 {0, 0}, {10, 10}, {17, 17}, {18, 19}, {30, 31}, 154 } 155 for _, p := range utf16Indices { 156 if want, got := p.UTF16, s.UTF16Index(p.Runes); want != got { 157 t.Errorf("UTF16Index(%d) = %d, wanted %d", p.Runes, got, want) 158 } 159 if want, got := p.Runes, s.RunesIndex(p.UTF16); want != got { 160 t.Errorf("RunesIndex(%d) = %d, wanted %d", p.UTF16, got, want) 161 } 162 } 163 }