...

Source file src/code.rocket9labs.com/tslocum/etk/kibodo/keyboard.go

Documentation: code.rocket9labs.com/tslocum/etk/kibodo

     1  package kibodo
     2  
     3  import (
     4  	"image"
     5  	"image/color"
     6  	"time"
     7  
     8  	"github.com/hajimehoshi/ebiten/v2"
     9  	"github.com/hajimehoshi/ebiten/v2/inpututil"
    10  	"github.com/hajimehoshi/ebiten/v2/text/v2"
    11  )
    12  
    13  // Keyboard is an on-screen keyboard widget.
    14  type Keyboard struct {
    15  	x, y int
    16  	w, h int
    17  
    18  	visible      bool
    19  	alpha        float64
    20  	passPhysical bool
    21  
    22  	incomingBuffer []rune
    23  
    24  	inputEvents []*Input
    25  
    26  	keys         [][]*Key
    27  	normalKeys   [][]*Key
    28  	extendedKeys [][]*Key
    29  	showExtended bool
    30  
    31  	backgroundLower *ebiten.Image
    32  	backgroundUpper *ebiten.Image
    33  	backgroundDirty bool
    34  
    35  	op *ebiten.DrawImageOptions
    36  
    37  	backgroundColor     color.RGBA
    38  	lastBackgroundColor color.RGBA
    39  
    40  	shift bool
    41  
    42  	touchIDs    []ebiten.TouchID
    43  	holdTouchID ebiten.TouchID
    44  	holdKey     *Key
    45  	wasPressed  bool
    46  
    47  	hideShortcuts []ebiten.Key
    48  
    49  	labelFont  *text.GoTextFace
    50  	lineHeight int
    51  	lineOffset int
    52  
    53  	backspaceDelay  time.Duration
    54  	backspaceRepeat time.Duration
    55  	backspaceLast   time.Time
    56  
    57  	scheduleFrameFunc func()
    58  }
    59  
    60  // NewKeyboard returns a new Keyboard widget.
    61  func NewKeyboard(f *text.GoTextFaceSource) *Keyboard {
    62  	k := &Keyboard{
    63  		alpha: 1.0,
    64  		op: &ebiten.DrawImageOptions{
    65  			Filter: ebiten.FilterNearest,
    66  		},
    67  		keys:            KeysQWERTY,
    68  		normalKeys:      KeysQWERTY,
    69  		backgroundLower: ebiten.NewImage(1, 1),
    70  		backgroundUpper: ebiten.NewImage(1, 1),
    71  		backgroundColor: color.RGBA{0, 0, 0, 255},
    72  		holdTouchID:     -1,
    73  		hideShortcuts:   []ebiten.Key{ebiten.KeyEscape},
    74  		labelFont:       fontFace(f, 64),
    75  		backspaceDelay:  500 * time.Millisecond,
    76  		backspaceRepeat: 75 * time.Millisecond,
    77  	}
    78  	k.fontUpdated()
    79  	return k
    80  }
    81  
    82  func fontFace(source *text.GoTextFaceSource, size float64) *text.GoTextFace {
    83  	return &text.GoTextFace{
    84  		Source: source,
    85  		Size:   size,
    86  	}
    87  }
    88  
    89  // SetRect sets the position and size of the widget.
    90  func (k *Keyboard) SetRect(x, y, w, h int) {
    91  	if k.x == x && k.y == y && k.w == w && k.h == h {
    92  		return
    93  	}
    94  	k.x, k.y, k.w, k.h = x, y, w, h
    95  
    96  	k.updateKeyRects()
    97  	k.backgroundDirty = true
    98  }
    99  
   100  // Rect returns the position and size of the widget.
   101  func (k *Keyboard) Rect() image.Rectangle {
   102  	return image.Rect(k.x, k.y, k.x+k.w, k.y+k.h)
   103  }
   104  
   105  // GetKeys returns the keys of the keyboard.
   106  func (k *Keyboard) GetKeys() [][]*Key {
   107  	return k.keys
   108  }
   109  
   110  // SetKeys sets the keys of the keyboard.
   111  func (k *Keyboard) SetKeys(keys [][]*Key) {
   112  	k.normalKeys = keys
   113  
   114  	if !k.showExtended && !keysEqual(keys, k.keys) {
   115  		k.keys = keys
   116  		k.updateKeyRects()
   117  		k.backgroundDirty = true
   118  	}
   119  }
   120  
   121  // SetExtendedKeys sets the keys of the keyboard when the .
   122  func (k *Keyboard) SetExtendedKeys(keys [][]*Key) {
   123  	k.extendedKeys = keys
   124  
   125  	if k.showExtended && !keysEqual(keys, k.keys) {
   126  		k.keys = keys
   127  		k.updateKeyRects()
   128  		k.backgroundDirty = true
   129  	}
   130  }
   131  
   132  // SetShowExtended sets whether the normal or extended keyboard is shown.
   133  func (k *Keyboard) SetShowExtended(show bool) {
   134  	if k.showExtended == show {
   135  		return
   136  	}
   137  	k.showExtended = show
   138  	if k.showExtended {
   139  		k.keys = k.extendedKeys
   140  	} else {
   141  		k.keys = k.normalKeys
   142  	}
   143  	k.updateKeyRects()
   144  	k.backgroundDirty = true
   145  }
   146  
   147  // SetLabelFont sets the key label font.
   148  func (k *Keyboard) SetLabelFont(face *text.GoTextFace) {
   149  	k.labelFont = face
   150  	k.fontUpdated()
   151  
   152  	k.backgroundDirty = true
   153  }
   154  
   155  func (k *Keyboard) fontUpdated() {
   156  	m := k.labelFont.Metrics()
   157  	k.lineHeight = int(m.HAscent + m.HDescent)
   158  	k.lineOffset = int(m.CapHeight)
   159  	if k.lineOffset < 0 {
   160  		k.lineOffset *= -1
   161  	}
   162  }
   163  
   164  // SetHideShortcuts sets the key shortcuts which, when pressed, will hide the
   165  // keyboard. Defaults to the Escape key.
   166  func (k *Keyboard) SetHideShortcuts(shortcuts []ebiten.Key) {
   167  	k.hideShortcuts = shortcuts
   168  }
   169  
   170  func (k *Keyboard) updateKeyRects() {
   171  	if len(k.keys) == 0 {
   172  		return
   173  	}
   174  
   175  	maxCells := 0
   176  	for _, rowKeys := range k.keys {
   177  		if len(rowKeys) > maxCells {
   178  			maxCells = len(rowKeys)
   179  		}
   180  	}
   181  
   182  	// TODO user configurable
   183  	cellPaddingW := 1
   184  	cellPaddingH := 1
   185  
   186  	cellH := (k.h - (cellPaddingH * (len(k.keys) - 1))) / len(k.keys)
   187  
   188  	row := 0
   189  	x, y := 0, 0
   190  	for _, rowKeys := range k.keys {
   191  		if len(rowKeys) == 0 {
   192  			continue
   193  		}
   194  
   195  		availableWidth := k.w
   196  		for _, key := range rowKeys {
   197  			if key.Wide {
   198  				availableWidth = availableWidth / 2
   199  				break
   200  			}
   201  		}
   202  
   203  		cellW := (availableWidth - (cellPaddingW * (len(rowKeys) - 1))) / len(rowKeys)
   204  
   205  		x = 0
   206  		for i, key := range rowKeys {
   207  			key.w, key.h = cellW, cellH
   208  			key.x, key.y = x, y
   209  
   210  			if i == len(rowKeys)-1 {
   211  				key.w = k.w - key.x
   212  			}
   213  
   214  			if key.Wide {
   215  				key.w = k.w - k.w/2 + (cellW)
   216  			}
   217  
   218  			x += key.w
   219  		}
   220  
   221  		// Count non-empty rows only
   222  		row++
   223  		y += (cellH + cellPaddingH)
   224  	}
   225  }
   226  
   227  func (k *Keyboard) at(x, y int) *Key {
   228  	if !k.visible {
   229  		return nil
   230  	}
   231  	if x >= k.x && x <= k.x+k.w && y >= k.y && y <= k.y+k.h {
   232  		x, y = x-k.x, y-k.y // Offset
   233  		for _, rowKeys := range k.keys {
   234  			for _, key := range rowKeys {
   235  				if x >= key.x && x <= key.x+key.w && y >= key.y && y <= key.y+key.h {
   236  					return key
   237  				}
   238  			}
   239  		}
   240  	}
   241  	return nil
   242  }
   243  
   244  // KeyAt returns the key located at the specified position, or nil if no key is found.
   245  func (k *Keyboard) KeyAt(x, y int) *Key {
   246  	return k.at(x, y)
   247  }
   248  
   249  func (k *Keyboard) handleToggleExtendedKey(inputKey ebiten.Key) bool {
   250  	if inputKey != KeyToggleExtended {
   251  		return false
   252  	}
   253  	k.showExtended = !k.showExtended
   254  	if k.showExtended {
   255  		k.keys = k.extendedKeys
   256  	} else {
   257  		k.keys = k.normalKeys
   258  	}
   259  	k.updateKeyRects()
   260  	k.backgroundDirty = true
   261  	return true
   262  }
   263  
   264  func (k *Keyboard) handleHideKey(inputKey ebiten.Key) bool {
   265  	for _, key := range k.hideShortcuts {
   266  		if key == inputKey {
   267  			k.Hide()
   268  			return true
   269  		}
   270  	}
   271  	return false
   272  }
   273  
   274  // Hit handles a key press.
   275  func (k *Keyboard) Hit(key *Key) {
   276  	now := time.Now()
   277  	if !key.pressedTime.IsZero() && now.Sub(key.pressedTime) < 50*time.Millisecond {
   278  		return
   279  	}
   280  	key.pressedTime = now
   281  	key.repeatTime = now.Add(k.backspaceDelay)
   282  
   283  	input := key.LowerInput
   284  	if k.shift {
   285  		input = key.UpperInput
   286  	}
   287  
   288  	if input.Key == ebiten.KeyShift {
   289  		k.shift = !k.shift
   290  		if k.scheduleFrameFunc != nil {
   291  			k.scheduleFrameFunc()
   292  		}
   293  		return
   294  	} else if k.handleToggleExtendedKey(input.Key) || k.handleHideKey(input.Key) {
   295  		return
   296  	}
   297  
   298  	k.inputEvents = append(k.inputEvents, input)
   299  }
   300  
   301  // HandleMouse passes the specified mouse event to the on-screen keyboard.
   302  func (k *Keyboard) HandleMouse(cursor image.Point, pressed bool, clicked bool) (handled bool, err error) {
   303  	if k.backgroundDirty {
   304  		k.drawBackground()
   305  		k.backgroundDirty = false
   306  	}
   307  
   308  	//pressDuration := 50 * time.Millisecond
   309  	if clicked {
   310  		var key *Key
   311  		if cursor.X != 0 || cursor.Y != 0 {
   312  			key = k.at(cursor.X, cursor.Y)
   313  		}
   314  		for _, rowKeys := range k.keys {
   315  			for _, rowKey := range rowKeys {
   316  				if key != nil && rowKey == key {
   317  					continue
   318  				}
   319  				rowKey.pressed = false
   320  			}
   321  		}
   322  		if key != nil {
   323  			key.pressed = true
   324  
   325  			k.Hit(key)
   326  		}
   327  		k.wasPressed = true
   328  	} else if pressed {
   329  		key := k.at(cursor.X, cursor.Y)
   330  		if key != nil {
   331  			// Repeat backspace and delete operations.
   332  			input := key.LowerInput
   333  			if k.shift {
   334  				input = key.UpperInput
   335  			}
   336  			if input.Key == ebiten.KeyBackspace || input.Key == ebiten.KeyDelete {
   337  				for time.Since(key.repeatTime) >= k.backspaceRepeat {
   338  					k.inputEvents = append(k.inputEvents, &Input{Key: input.Key})
   339  					key.repeatTime = key.repeatTime.Add(k.backspaceRepeat)
   340  				}
   341  			}
   342  
   343  			key.pressed = true
   344  			k.wasPressed = true
   345  
   346  			for _, rowKeys := range k.keys {
   347  				for _, rowKey := range rowKeys {
   348  					if rowKey == key || !rowKey.pressed {
   349  						continue
   350  					}
   351  					rowKey.pressed = false
   352  				}
   353  			}
   354  		}
   355  	} else if k.wasPressed {
   356  		for _, rowKeys := range k.keys {
   357  			for _, rowKey := range rowKeys {
   358  				if !rowKey.pressed {
   359  					continue
   360  				}
   361  				rowKey.pressed = false
   362  			}
   363  		}
   364  	}
   365  	return true, nil
   366  }
   367  
   368  // Update handles user input. This function is called by Ebitengine.
   369  func (k *Keyboard) Update() error {
   370  	if !k.visible {
   371  		return nil
   372  	}
   373  
   374  	if k.backgroundDirty {
   375  		k.drawBackground()
   376  		k.backgroundDirty = false
   377  	}
   378  
   379  	// Pass through physical keyboard input
   380  	if k.passPhysical {
   381  		// Read input characters
   382  		k.incomingBuffer = ebiten.AppendInputChars(k.incomingBuffer[:0])
   383  		if len(k.incomingBuffer) > 0 {
   384  			for _, r := range k.incomingBuffer {
   385  				k.inputEvents = append(k.inputEvents, &Input{Rune: r}) // Pass through
   386  			}
   387  		} else {
   388  			// Read keys
   389  			for _, key := range allKeys {
   390  				if inpututil.IsKeyJustPressed(key) {
   391  					if k.handleHideKey(key) {
   392  						// Hidden
   393  						return nil
   394  					}
   395  					k.inputEvents = append(k.inputEvents, &Input{Key: key}) // Pass through
   396  				}
   397  			}
   398  		}
   399  	}
   400  
   401  	// Handle mouse input
   402  	var key *Key
   403  	pressDuration := 50 * time.Millisecond
   404  	if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
   405  		x, y := ebiten.CursorPosition()
   406  
   407  		key = k.at(x, y)
   408  		if key != nil {
   409  			for _, rowKeys := range k.keys {
   410  				for _, rowKey := range rowKeys {
   411  					rowKey.pressed = false
   412  				}
   413  			}
   414  			key.pressed = true
   415  
   416  			k.Hit(key)
   417  
   418  			go func() {
   419  				time.Sleep(pressDuration)
   420  
   421  				key.pressed = false
   422  				if k.scheduleFrameFunc != nil {
   423  					k.scheduleFrameFunc()
   424  				}
   425  			}()
   426  		}
   427  
   428  		k.wasPressed = true
   429  	} else if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
   430  		x, y := ebiten.CursorPosition()
   431  
   432  		key = k.at(x, y)
   433  		if key != nil {
   434  			if !key.pressed {
   435  				input := key.LowerInput
   436  				if k.shift {
   437  					input = key.UpperInput
   438  				}
   439  
   440  				// Repeat backspace and delete operations.
   441  				if input.Key == ebiten.KeyBackspace || input.Key == ebiten.KeyDelete {
   442  					for time.Since(key.repeatTime) >= k.backspaceRepeat {
   443  						k.inputEvents = append(k.inputEvents, &Input{Key: input.Key})
   444  						key.repeatTime = key.repeatTime.Add(k.backspaceRepeat)
   445  					}
   446  				}
   447  			}
   448  			key.pressed = true
   449  
   450  			for _, rowKeys := range k.keys {
   451  				for _, rowKey := range rowKeys {
   452  					if rowKey == key || !rowKey.pressed {
   453  						continue
   454  					}
   455  					rowKey.pressed = false
   456  				}
   457  			}
   458  		}
   459  
   460  		k.wasPressed = true
   461  	}
   462  
   463  	// Handle touch input
   464  	if k.holdTouchID != -1 {
   465  		x, y := ebiten.TouchPosition(k.holdTouchID)
   466  		if x == 0 && y == 0 {
   467  			k.holdTouchID = -1
   468  		} else {
   469  			key = k.at(x, y)
   470  			if key != k.holdKey {
   471  				k.holdTouchID = -1
   472  				return nil
   473  			}
   474  			//k.Hold(key)
   475  			k.holdKey = key
   476  			k.wasPressed = true
   477  		}
   478  	}
   479  	if k.holdTouchID == -1 {
   480  		k.touchIDs = inpututil.AppendJustPressedTouchIDs(k.touchIDs[:0])
   481  		for _, id := range k.touchIDs {
   482  			x, y := ebiten.TouchPosition(id)
   483  
   484  			key = k.at(x, y)
   485  			if key != nil {
   486  				input := key.LowerInput
   487  				if k.shift {
   488  					input = key.UpperInput
   489  				}
   490  
   491  				if !key.pressed {
   492  					key.pressed = true
   493  					key.pressedTouchID = id
   494  
   495  					for _, rowKeys := range k.keys {
   496  						for _, rowKey := range rowKeys {
   497  							if rowKey != key && rowKey.pressed {
   498  								rowKey.pressed = false
   499  							}
   500  						}
   501  					}
   502  
   503  					k.Hit(key)
   504  					k.holdTouchID = id
   505  					k.holdKey = key
   506  
   507  					// Repeat backspace and delete operations.
   508  					if input.Key == ebiten.KeyBackspace || input.Key == ebiten.KeyDelete {
   509  						k.backspaceLast = time.Now().Add(k.backspaceDelay)
   510  					}
   511  				} else {
   512  					// Repeat backspace and delete operations.
   513  					if input.Key == ebiten.KeyBackspace || input.Key == ebiten.KeyDelete {
   514  						for time.Since(k.backspaceLast) >= k.backspaceRepeat {
   515  							k.inputEvents = append(k.inputEvents, &Input{Key: input.Key})
   516  							k.backspaceLast = k.backspaceLast.Add(k.backspaceRepeat)
   517  						}
   518  					}
   519  				}
   520  				k.wasPressed = true
   521  			}
   522  		}
   523  	}
   524  	for _, rowKeys := range k.keys {
   525  		for _, rowKey := range rowKeys {
   526  			if rowKey == key || !rowKey.pressed {
   527  				continue
   528  			}
   529  			rowKey.pressed = false
   530  		}
   531  	}
   532  	return nil
   533  }
   534  
   535  func (k *Keyboard) drawBackground() {
   536  	if k.w == 0 || k.h == 0 {
   537  		return
   538  	}
   539  
   540  	if !k.backgroundLower.Bounds().Eq(image.Rect(0, 0, k.w, k.h)) || !k.backgroundUpper.Bounds().Eq(image.Rect(0, 0, k.w, k.h)) || k.backgroundColor != k.lastBackgroundColor {
   541  		k.backgroundLower = ebiten.NewImage(k.w, k.h)
   542  		k.backgroundUpper = ebiten.NewImage(k.w, k.h)
   543  		k.lastBackgroundColor = k.backgroundColor
   544  	}
   545  	k.backgroundLower.Fill(k.backgroundColor)
   546  	k.backgroundUpper.Fill(k.backgroundColor)
   547  
   548  	halfLineHeight := k.lineHeight / 2
   549  
   550  	lightShade := color.RGBA{150, 150, 150, 255}
   551  	darkShade := color.RGBA{30, 30, 30, 255}
   552  
   553  	var keyImage *ebiten.Image
   554  	for i := 0; i < 2; i++ {
   555  		shift := i == 1
   556  		img := k.backgroundLower
   557  		if shift {
   558  			img = k.backgroundUpper
   559  		}
   560  		for _, rowKeys := range k.keys {
   561  			for _, key := range rowKeys {
   562  				r := image.Rect(key.x, key.y, key.x+key.w, key.y+key.h)
   563  				keyImage = img.SubImage(r).(*ebiten.Image)
   564  
   565  				// Draw key background
   566  				// TODO configurable
   567  				keyImage.Fill(color.RGBA{90, 90, 90, 255})
   568  
   569  				// Draw key label
   570  				label := key.LowerLabel
   571  				if shift {
   572  					label = key.UpperLabel
   573  				}
   574  
   575  				boundsW, boundsY := text.Measure(label, k.labelFont, float64(k.lineHeight))
   576  				x := key.w/2 - int(boundsW)/2
   577  				if x < 0 {
   578  					x = 0
   579  				}
   580  				y := key.h/2 - int(boundsY)/2
   581  				_ = halfLineHeight
   582  				op := &text.DrawOptions{}
   583  				op.GeoM.Translate(float64(key.x+x), float64(key.y+y))
   584  				op.ColorScale.ScaleWithColor(color.White)
   585  				text.Draw(keyImage, label, k.labelFont, op)
   586  
   587  				// Draw border
   588  				keyImage.SubImage(image.Rect(key.x, key.y, key.x+key.w, key.y+1)).(*ebiten.Image).Fill(lightShade)
   589  				keyImage.SubImage(image.Rect(key.x, key.y, key.x+1, key.y+key.h)).(*ebiten.Image).Fill(lightShade)
   590  				keyImage.SubImage(image.Rect(key.x, key.y+key.h-1, key.x+key.w, key.y+key.h)).(*ebiten.Image).Fill(darkShade)
   591  				keyImage.SubImage(image.Rect(key.x+key.w-1, key.y, key.x+key.w, key.y+key.h)).(*ebiten.Image).Fill(darkShade)
   592  			}
   593  		}
   594  	}
   595  }
   596  
   597  // Draw draws the widget on the provided image.  This function is called by Ebitengine.
   598  func (k *Keyboard) Draw(target *ebiten.Image) {
   599  	if !k.visible {
   600  		return
   601  	}
   602  
   603  	if k.backgroundDirty {
   604  		k.drawBackground()
   605  		k.backgroundDirty = false
   606  	}
   607  
   608  	var background *ebiten.Image
   609  	if !k.shift {
   610  		background = k.backgroundLower
   611  	} else {
   612  		background = k.backgroundUpper
   613  	}
   614  
   615  	k.op.GeoM.Reset()
   616  	k.op.GeoM.Translate(float64(k.x), float64(k.y))
   617  	k.op.ColorM.Scale(1, 1, 1, k.alpha)
   618  	target.DrawImage(background, k.op)
   619  	k.op.ColorM.Reset()
   620  
   621  	// Draw pressed keys
   622  	for _, rowKeys := range k.keys {
   623  		for _, key := range rowKeys {
   624  			if !key.pressed {
   625  				continue
   626  			}
   627  
   628  			// TODO buffer to prevent issues with alpha channel
   629  			k.op.GeoM.Reset()
   630  			k.op.GeoM.Translate(float64(k.x+key.x), float64(k.y+key.y))
   631  			k.op.ColorM.Scale(0.75, 0.75, 0.75, k.alpha)
   632  
   633  			target.DrawImage(background.SubImage(image.Rect(key.x, key.y, key.x+key.w, key.y+key.h)).(*ebiten.Image), k.op)
   634  			k.op.ColorM.Reset()
   635  
   636  			// Draw shadow.
   637  			darkShade := color.RGBA{60, 60, 60, 255}
   638  			subImg := target.SubImage(image.Rect(k.x+key.x, k.y+key.y, k.x+key.x+key.w, k.y+key.y+1)).(*ebiten.Image)
   639  			subImg.Fill(darkShade)
   640  			subImg = target.SubImage(image.Rect(k.x+key.x, k.y+key.y, k.x+key.x+1, k.y+key.y+key.h)).(*ebiten.Image)
   641  			subImg.Fill(darkShade)
   642  		}
   643  	}
   644  }
   645  
   646  // SetPassThroughPhysicalInput sets a flag that controls whether physical
   647  // keyboard input is passed through to the widget's input buffer. This is not
   648  // enabled by default.
   649  func (k *Keyboard) SetPassThroughPhysicalInput(pass bool) {
   650  	k.passPhysical = pass
   651  }
   652  
   653  // SetAlpha sets the transparency level of the widget on a scale of 0 to 1.0.
   654  func (k *Keyboard) SetAlpha(alpha float64) {
   655  	k.alpha = alpha
   656  }
   657  
   658  // Show shows the widget.
   659  func (k *Keyboard) Show() {
   660  	k.visible = true
   661  }
   662  
   663  // Visible returns whether the widget is currently shown.
   664  func (k *Keyboard) Visible() bool {
   665  	return k.visible
   666  }
   667  
   668  // Hide hides the widget.
   669  func (k *Keyboard) Hide() {
   670  	k.visible = false
   671  	if k.showExtended {
   672  		k.showExtended = false
   673  		k.keys = k.normalKeys
   674  		k.updateKeyRects()
   675  		k.backgroundDirty = true
   676  	}
   677  }
   678  
   679  // AppendInput appends user input that was received since the function was last called.
   680  func (k *Keyboard) AppendInput(events []*Input) []*Input {
   681  	events = append(events, k.inputEvents...)
   682  	k.inputEvents = nil
   683  	return events
   684  }
   685  
   686  // SetScheduleFrameFunc sets the function called whenever the screen should be redrawn.
   687  func (k *Keyboard) SetScheduleFrameFunc(f func()) {
   688  	k.scheduleFrameFunc = f
   689  }
   690  
   691  func keysEqual(a [][]*Key, b [][]*Key) bool {
   692  	if len(a) != len(b) {
   693  		return false
   694  	}
   695  	for i := range a {
   696  		if len(a[i]) != len(b[i]) {
   697  			return false
   698  		}
   699  		for j := range b[i] {
   700  			if a[i][j] != b[i][j] {
   701  				return false
   702  			}
   703  		}
   704  	}
   705  	return true
   706  }
   707  

View as plain text