...

Source file src/code.rocketnine.space/tslocum/kibodo/keyboard.go

Documentation: code.rocketnine.space/tslocum/kibodo

     1  package kibodo
     2  
     3  import (
     4  	"image"
     5  	"image/color"
     6  	"log"
     7  	"time"
     8  
     9  	"github.com/hajimehoshi/ebiten/v2"
    10  	"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
    11  	"github.com/hajimehoshi/ebiten/v2/inpututil"
    12  	"github.com/hajimehoshi/ebiten/v2/text"
    13  	"golang.org/x/image/font"
    14  	"golang.org/x/image/font/opentype"
    15  )
    16  
    17  // Keyboard is an on-screen keyboard widget.
    18  type Keyboard struct {
    19  	x, y int
    20  	w, h int
    21  
    22  	visible       bool
    23  	alpha         float64
    24  	passPhysical  bool
    25  	allowUserHide bool
    26  
    27  	incomingBuffer []rune
    28  
    29  	inputEvents []*Input
    30  
    31  	keys [][]*Key
    32  
    33  	backgroundLower *ebiten.Image
    34  	backgroundUpper *ebiten.Image
    35  	backgroundDirty bool
    36  
    37  	op *ebiten.DrawImageOptions
    38  
    39  	backgroundColor     color.Color
    40  	lastBackgroundColor color.Color
    41  
    42  	shift bool
    43  
    44  	touchIDs []ebiten.TouchID
    45  
    46  	hideShortcuts []ebiten.Key
    47  
    48  	labelFont font.Face
    49  }
    50  
    51  // NewKeyboard returns a new Keyboard widget.
    52  func NewKeyboard() *Keyboard {
    53  	fontFace, err := defaultFontFace(64)
    54  	if err != nil {
    55  		log.Fatal(err)
    56  	}
    57  
    58  	k := &Keyboard{
    59  		alpha: 1.0,
    60  		op: &ebiten.DrawImageOptions{
    61  			Filter: ebiten.FilterNearest,
    62  		},
    63  		keys:            KeysQWERTY,
    64  		backgroundLower: ebiten.NewImage(1, 1),
    65  		backgroundUpper: ebiten.NewImage(1, 1),
    66  		backgroundColor: color.Black,
    67  		labelFont:       fontFace,
    68  	}
    69  	return k
    70  }
    71  
    72  func defaultFont() (*opentype.Font, error) {
    73  	return opentype.Parse(fonts.MPlus1pRegular_ttf)
    74  }
    75  
    76  func defaultFontFace(size float64) (font.Face, error) {
    77  	f, err := defaultFont()
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  	const dpi = 72 // TODO
    82  	return opentype.NewFace(f, &opentype.FaceOptions{
    83  		Size:    size,
    84  		DPI:     dpi,
    85  		Hinting: font.HintingFull,
    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  // GetKeys returns the keys of the keyboard.
   101  func (k *Keyboard) GetKeys() [][]*Key {
   102  	return k.keys
   103  }
   104  
   105  // SetKeys sets the keys of the keyboard.
   106  func (k *Keyboard) SetKeys(keys [][]*Key) {
   107  	k.keys = keys
   108  
   109  	k.updateKeyRects()
   110  	k.backgroundDirty = true
   111  }
   112  
   113  // SetLabelFont sets the key label font.
   114  func (k *Keyboard) SetLabelFont(face font.Face) {
   115  	k.labelFont = face
   116  
   117  	k.backgroundDirty = true
   118  }
   119  
   120  // SetHideShortcuts sets the key shortcuts which, when pressed, will hide the
   121  // keyboard.
   122  func (k *Keyboard) SetHideShortcuts(shortcuts []ebiten.Key) {
   123  	k.hideShortcuts = shortcuts
   124  }
   125  
   126  func (k *Keyboard) updateKeyRects() {
   127  	if len(k.keys) == 0 {
   128  		return
   129  	}
   130  
   131  	maxCells := 0
   132  	for _, rowKeys := range k.keys {
   133  		if len(rowKeys) > maxCells {
   134  			maxCells = len(rowKeys)
   135  		}
   136  	}
   137  
   138  	// TODO user configurable
   139  	cellPaddingW := 1
   140  	cellPaddingH := 1
   141  
   142  	cellH := (k.h - (cellPaddingH * (len(k.keys) - 1))) / len(k.keys)
   143  
   144  	row := 0
   145  	for _, rowKeys := range k.keys {
   146  		if len(rowKeys) == 0 {
   147  			continue
   148  		}
   149  
   150  		cellW := (k.w - (cellPaddingW * (len(rowKeys) - 1))) / len(rowKeys)
   151  
   152  		for i, key := range rowKeys {
   153  			key.x = (cellW + cellPaddingW) * i
   154  			key.y = (cellH + cellPaddingH) * row
   155  			key.w = cellW
   156  			key.h = cellH
   157  
   158  			if i == len(rowKeys)-1 {
   159  				key.w = k.w - key.x
   160  			}
   161  		}
   162  
   163  		// Count non-empty rows only
   164  		row++
   165  	}
   166  }
   167  
   168  func (k *Keyboard) at(x, y int) *Key {
   169  	if !k.visible {
   170  		return nil
   171  	}
   172  	if x >= k.x && x <= k.x+k.w && y >= k.y && y <= k.y+k.h {
   173  		x, y = x-k.x, y-k.y // Offset
   174  		for _, rowKeys := range k.keys {
   175  			for _, key := range rowKeys {
   176  				if x >= key.x && x <= key.x+key.w && y >= key.y && y <= key.y+key.h {
   177  					return key
   178  				}
   179  			}
   180  		}
   181  	}
   182  	return nil
   183  }
   184  
   185  func (k *Keyboard) handleHideKey(inputKey ebiten.Key) bool {
   186  	if !k.allowUserHide {
   187  		return false
   188  	}
   189  
   190  	for _, key := range k.hideShortcuts {
   191  		if key == inputKey {
   192  			k.Hide()
   193  			return true
   194  		}
   195  	}
   196  	return false
   197  }
   198  
   199  // Hit handles a key press.
   200  func (k *Keyboard) Hit(key *Key) {
   201  	input := key.LowerInput
   202  	if k.shift {
   203  		input = key.UpperInput
   204  	}
   205  
   206  	if input.Key == ebiten.KeyShift {
   207  		k.shift = !k.shift
   208  		ebiten.ScheduleFrame()
   209  		return
   210  	} else if k.handleHideKey(input.Key) {
   211  		// Hidden
   212  		return
   213  	}
   214  
   215  	k.inputEvents = append(k.inputEvents, input)
   216  }
   217  
   218  // Update handles user input. This function is called by Ebitengine.
   219  func (k *Keyboard) Update() error {
   220  	if !k.visible {
   221  		return nil
   222  	}
   223  
   224  	// Pass through physical keyboard input
   225  	if k.passPhysical {
   226  		// Read input characters
   227  		k.incomingBuffer = ebiten.AppendInputChars(k.incomingBuffer[:0])
   228  		if len(k.incomingBuffer) > 0 {
   229  			for _, r := range k.incomingBuffer {
   230  				k.inputEvents = append(k.inputEvents, &Input{Rune: r}) // Pass through
   231  			}
   232  		} else {
   233  			// Read keys
   234  			for _, key := range allKeys {
   235  				if inpututil.IsKeyJustPressed(key) {
   236  					if k.handleHideKey(key) {
   237  						// Hidden
   238  						return nil
   239  					}
   240  					k.inputEvents = append(k.inputEvents, &Input{Key: key}) // Pass through
   241  				}
   242  			}
   243  		}
   244  	}
   245  	// Handle mouse input
   246  	pressDuration := 50 * time.Millisecond
   247  	if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) {
   248  		x, y := ebiten.CursorPosition()
   249  
   250  		key := k.at(x, y)
   251  		if key != nil {
   252  			for _, rowKeys := range k.keys {
   253  				for _, rowKey := range rowKeys {
   254  					rowKey.pressed = false
   255  				}
   256  			}
   257  			key.pressed = true
   258  
   259  			k.Hit(key)
   260  
   261  			// TODO replace with pressUntil
   262  			go func() {
   263  				time.Sleep(pressDuration)
   264  
   265  				key.pressed = false
   266  				ebiten.ScheduleFrame()
   267  			}()
   268  		}
   269  	} else if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
   270  		x, y := ebiten.CursorPosition()
   271  
   272  		key := k.at(x, y)
   273  		if key != nil {
   274  			key.pressed = true
   275  
   276  			for _, rowKeys := range k.keys {
   277  				for _, rowKey := range rowKeys {
   278  					if rowKey == key || !rowKey.pressed {
   279  						continue
   280  					}
   281  					rowKey.pressed = false
   282  				}
   283  			}
   284  		}
   285  	}
   286  	// Handle touch input
   287  	k.touchIDs = inpututil.AppendJustPressedTouchIDs(k.touchIDs[:0])
   288  	for _, id := range k.touchIDs {
   289  		x, y := ebiten.TouchPosition(id)
   290  
   291  		key := k.at(x, y)
   292  		if key != nil && !key.pressed {
   293  			key.pressed = true
   294  			key.pressedTouchID = id
   295  
   296  			for _, rowKeys := range k.keys {
   297  				for _, rowKey := range rowKeys {
   298  					if rowKey != key && rowKey.pressed {
   299  						rowKey.pressed = false
   300  					}
   301  				}
   302  			}
   303  
   304  			k.Hit(key)
   305  
   306  			go func() {
   307  				var touchIDs []ebiten.TouchID
   308  				t := time.NewTicker(pressDuration)
   309  				for range t.C {
   310  					touchIDs = ebiten.AppendTouchIDs(touchIDs[:0])
   311  
   312  					var found bool
   313  					for _, touchID := range touchIDs {
   314  						if id == touchID {
   315  							found = true
   316  							break
   317  						}
   318  					}
   319  
   320  					if found {
   321  						tx, ty := ebiten.TouchPosition(id)
   322  						if tx != 0 || ty != 0 {
   323  							x, y = tx, ty
   324  						}
   325  					}
   326  
   327  					if !found {
   328  						key.pressed = false
   329  						ebiten.ScheduleFrame()
   330  						return
   331  					}
   332  				}
   333  			}()
   334  		}
   335  	}
   336  	return nil
   337  }
   338  
   339  func (k *Keyboard) offset(x, y int) (int, int) {
   340  	return x + k.x, y + k.y
   341  }
   342  
   343  func (k *Keyboard) drawBackground() {
   344  	if k.w == 0 || k.h == 0 {
   345  		return
   346  	}
   347  
   348  	if k.backgroundLower.Bounds() != image.Rect(0, 0, k.w, k.h) || k.backgroundUpper.Bounds() != image.Rect(0, 0, k.w, k.h) || k.backgroundColor != k.lastBackgroundColor {
   349  		k.backgroundLower = ebiten.NewImage(k.w, k.h)
   350  		k.backgroundLower.Fill(k.backgroundColor)
   351  
   352  		k.backgroundUpper = ebiten.NewImage(k.w, k.h)
   353  		k.backgroundUpper.Fill(k.backgroundColor)
   354  
   355  		k.lastBackgroundColor = k.backgroundColor
   356  	}
   357  
   358  	var img *ebiten.Image
   359  	for i := 0; i < 2; i++ {
   360  		shift := i == 1
   361  		for _, rowKeys := range k.keys {
   362  			for _, key := range rowKeys {
   363  				if img == nil {
   364  					img = ebiten.NewImage(key.w, key.h)
   365  				} else {
   366  					bounds := img.Bounds()
   367  					if bounds.Dx() != key.w || bounds.Dy() != key.h {
   368  						img = ebiten.NewImage(key.w, key.h)
   369  					}
   370  				}
   371  
   372  				// Draw key background
   373  				// TODO configurable
   374  				img.Fill(color.RGBA{90, 90, 90, 255})
   375  
   376  				// Draw key label
   377  				label := key.LowerLabel
   378  				if shift {
   379  					label = key.UpperLabel
   380  				}
   381  
   382  				bounds := text.BoundString(k.labelFont, label)
   383  				x := (key.w - bounds.Dx()) / 2
   384  				if x < 0 {
   385  					x = 0
   386  				}
   387  				y := key.h / 2
   388  				text.Draw(img, label, k.labelFont, x, y, color.White)
   389  
   390  				// Draw border
   391  				lightShade := color.RGBA{150, 150, 150, 255}
   392  				darkShade := color.RGBA{30, 30, 30, 255}
   393  				for j := 0; j < key.w; j++ {
   394  					img.Set(j, 0, lightShade)
   395  					img.Set(j, key.h-1, darkShade)
   396  				}
   397  				for j := 0; j < key.h; j++ {
   398  					img.Set(0, j, lightShade)
   399  					img.Set(key.w-1, j, darkShade)
   400  				}
   401  
   402  				// Draw key
   403  				k.op.GeoM.Reset()
   404  				k.op.GeoM.Translate(float64(key.x), float64(key.y))
   405  
   406  				if !shift {
   407  					k.backgroundLower.DrawImage(img, k.op)
   408  				} else {
   409  					k.backgroundUpper.DrawImage(img, k.op)
   410  				}
   411  				k.op.ColorM.Reset()
   412  			}
   413  		}
   414  	}
   415  }
   416  
   417  // Draw draws the widget on the provided image.  This function is called by Ebitengine.
   418  func (k *Keyboard) Draw(target *ebiten.Image) {
   419  	if !k.visible {
   420  		return
   421  	}
   422  
   423  	if k.backgroundDirty {
   424  		k.drawBackground()
   425  		k.backgroundDirty = false
   426  	}
   427  
   428  	var background *ebiten.Image
   429  	if !k.shift {
   430  		background = k.backgroundLower
   431  	} else {
   432  		background = k.backgroundUpper
   433  	}
   434  
   435  	k.op.GeoM.Reset()
   436  	k.op.GeoM.Translate(float64(k.x), float64(k.y))
   437  	k.op.ColorM.Scale(1, 1, 1, k.alpha)
   438  	target.DrawImage(background, k.op)
   439  	k.op.ColorM.Reset()
   440  
   441  	// Draw pressed keys
   442  	for _, rowKeys := range k.keys {
   443  		for _, key := range rowKeys {
   444  			if !key.pressed {
   445  				continue
   446  			}
   447  
   448  			// TODO buffer to prevent issues with alpha channel
   449  			k.op.GeoM.Reset()
   450  			k.op.GeoM.Translate(float64(k.x+key.x), float64(k.y+key.y))
   451  			k.op.ColorM.Scale(0.75, 0.75, 0.75, k.alpha)
   452  
   453  			target.DrawImage(background.SubImage(image.Rect(key.x, key.y, key.x+key.w, key.y+key.h)).(*ebiten.Image), k.op)
   454  			k.op.ColorM.Reset()
   455  
   456  			// Draw shadow.
   457  			darkShade := color.RGBA{60, 60, 60, 255}
   458  			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)
   459  			subImg.Fill(darkShade)
   460  			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)
   461  			subImg.Fill(darkShade)
   462  		}
   463  	}
   464  }
   465  
   466  // SetAllowUserHide sets a flag that controls whether the widget may be hidden
   467  // by the user.
   468  func (k *Keyboard) SetAllowUserHide(allow bool) {
   469  	k.allowUserHide = allow
   470  }
   471  
   472  // SetPassThroughPhysicalInput sets a flag that controls whether physical
   473  // keyboard input is passed through to the widget's input buffer. This is not
   474  // enabled by default.
   475  func (k *Keyboard) SetPassThroughPhysicalInput(pass bool) {
   476  	k.passPhysical = pass
   477  }
   478  
   479  // SetAlpha sets the transparency level of the widget on a scale of 0 to 1.0.
   480  func (k *Keyboard) SetAlpha(alpha float64) {
   481  	k.alpha = alpha
   482  }
   483  
   484  // Show shows the widget.
   485  func (k *Keyboard) Show() {
   486  	k.visible = true
   487  }
   488  
   489  // Visible returns whether the widget is currently shown.
   490  func (k *Keyboard) Visible() bool {
   491  	return k.visible
   492  }
   493  
   494  // Hide hides the widget.
   495  func (k *Keyboard) Hide() {
   496  	k.visible = false
   497  }
   498  
   499  // AppendInput appends user input that was received since the function was last called.
   500  func (k *Keyboard) AppendInput(events []*Input) []*Input {
   501  	events = append(events, k.inputEvents...)
   502  	k.inputEvents = nil
   503  	return events
   504  }
   505  

View as plain text