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  }