...

Source file src/code.rocket9labs.com/tslocum/etk/game.go

Documentation: code.rocket9labs.com/tslocum/etk

     1  package etk
     2  
     3  import (
     4  	"fmt"
     5  	"image"
     6  	"image/color"
     7  	"log"
     8  	"math"
     9  	"sync"
    10  	"time"
    11  
    12  	"code.rocket9labs.com/tslocum/etk/messeji"
    13  	"github.com/hajimehoshi/ebiten/v2"
    14  	"github.com/hajimehoshi/ebiten/v2/inpututil"
    15  	"golang.org/x/image/font"
    16  	"golang.org/x/image/font/opentype"
    17  	"golang.org/x/image/font/sfnt"
    18  	"golang.org/x/image/math/fixed"
    19  )
    20  
    21  // Alignment specifies how text is aligned within the field.
    22  type Alignment int
    23  
    24  const (
    25  	// AlignStart aligns text at the start of the field.
    26  	AlignStart Alignment = 0
    27  
    28  	// AlignCenter aligns text at the center of the field.
    29  	AlignCenter Alignment = 1
    30  
    31  	// AlignEnd aligns text at the end of the field.
    32  	AlignEnd Alignment = 2
    33  )
    34  
    35  var root Widget
    36  
    37  var drawDebug bool
    38  
    39  var (
    40  	lastWidth, lastHeight int
    41  
    42  	lastX, lastY = -math.MaxInt, -math.MaxInt
    43  
    44  	touchIDs      []ebiten.TouchID
    45  	activeTouchID = ebiten.TouchID(-1)
    46  
    47  	focusedWidget Widget
    48  
    49  	pressedWidget Widget
    50  
    51  	cursorShape ebiten.CursorShapeType
    52  
    53  	lastBackspaceRepeat time.Time
    54  
    55  	keyBuffer  []ebiten.Key
    56  	runeBuffer []rune
    57  
    58  	fontMutex = &sync.Mutex{}
    59  )
    60  
    61  var debugColor = color.RGBA{0, 0, 255, 255}
    62  
    63  const (
    64  	backspaceRepeatWait = 500 * time.Millisecond
    65  	backspaceRepeatTime = 75 * time.Millisecond
    66  )
    67  
    68  var deviceScale float64
    69  
    70  // ScaleFactor returns the device scale factor. When running on Android, this function
    71  // may only be called during or after the first Layout call made by Ebitengine.
    72  func ScaleFactor() float64 {
    73  	if deviceScale == 0 {
    74  		monitor := ebiten.Monitor()
    75  		if monitor != nil {
    76  			deviceScale = monitor.DeviceScaleFactor()
    77  		}
    78  		if deviceScale <= 0 {
    79  			deviceScale = ebiten.DeviceScaleFactor()
    80  		}
    81  
    82  	}
    83  	return deviceScale
    84  }
    85  
    86  // Scale applies the device scale factor to the provided value and returns the result.
    87  // When running on Android, this function may only be called during or after the first
    88  // Layout call made by Ebitengine.
    89  func Scale(v int) int {
    90  	if deviceScale == 0 {
    91  		monitor := ebiten.Monitor()
    92  		if monitor != nil {
    93  			deviceScale = monitor.DeviceScaleFactor()
    94  		}
    95  		if deviceScale <= 0 {
    96  			deviceScale = ebiten.DeviceScaleFactor()
    97  		}
    98  
    99  	}
   100  	return int(float64(v) * deviceScale)
   101  }
   102  
   103  var (
   104  	fontCache     = make(map[string]font.Face)
   105  	fontCacheLock sync.Mutex
   106  )
   107  
   108  // FontFace returns a face for the provided font and size. Scaling is not applied.
   109  func FontFace(fnt *sfnt.Font, size int) font.Face {
   110  	id := fmt.Sprintf("%p/%d", fnt, size)
   111  
   112  	fontCacheLock.Lock()
   113  	defer fontCacheLock.Unlock()
   114  
   115  	f := fontCache[id]
   116  	if f != nil {
   117  		return f
   118  	}
   119  
   120  	const dpi = 72
   121  	f, err := opentype.NewFace(fnt, &opentype.FaceOptions{
   122  		Size:    float64(size),
   123  		DPI:     dpi,
   124  		Hinting: font.HintingFull,
   125  	})
   126  	if err != nil {
   127  		log.Fatal(err)
   128  	}
   129  
   130  	fontCache[id] = f
   131  	return f
   132  }
   133  
   134  // SetRoot sets the root widget. The root widget and all of its children will
   135  // be drawn on the screen and receive user input. The root widget will also be
   136  // focused. To temporarily disable etk, set a nil root widget.
   137  func SetRoot(w Widget) {
   138  	root = w
   139  	if root != nil && (lastWidth != 0 || lastHeight != 0) {
   140  		root.SetRect(image.Rect(0, 0, lastWidth, lastHeight))
   141  	}
   142  	SetFocus(root)
   143  }
   144  
   145  // SetFocus focuses a widget.
   146  func SetFocus(w Widget) {
   147  	lastFocused := focusedWidget
   148  	if w != nil && !w.SetFocus(true) {
   149  		return
   150  	}
   151  	if lastFocused != nil && lastFocused != w {
   152  		lastFocused.SetFocus(false)
   153  	}
   154  	focusedWidget = w
   155  }
   156  
   157  // Focused returns the currently focused widget. If no widget is focused, nil is returned.
   158  func Focused() Widget {
   159  	return focusedWidget
   160  }
   161  
   162  func int26ToRect(r fixed.Rectangle26_6) image.Rectangle {
   163  	x, y := r.Min.X, r.Min.Y
   164  	w, h := r.Max.X-r.Min.X, r.Max.Y-r.Min.Y
   165  	return image.Rect(x.Round(), y.Round(), (x + w).Round(), (y + h).Round())
   166  }
   167  
   168  // BoundString returns the bounds of the provided string.
   169  func BoundString(f font.Face, s string) image.Rectangle {
   170  	fontMutex.Lock()
   171  	defer fontMutex.Unlock()
   172  
   173  	bounds, _ := font.BoundString(f, s)
   174  	return int26ToRect(bounds)
   175  }
   176  
   177  // SetDebug sets whether debug information is drawn on screen. When enabled,
   178  // all visible widgets are outlined.
   179  func SetDebug(debug bool) {
   180  	drawDebug = debug
   181  }
   182  
   183  // ScreenSize returns the current screen size.
   184  func ScreenSize() (width int, height int) {
   185  	return lastWidth, lastHeight
   186  }
   187  
   188  // Layout sets the current screen size and resizes the root widget.
   189  func Layout(outsideWidth int, outsideHeight int) {
   190  	outsideWidth, outsideHeight = Scale(outsideWidth), Scale(outsideHeight)
   191  	if outsideWidth != lastWidth || outsideHeight != lastHeight {
   192  		lastWidth, lastHeight = outsideWidth, outsideHeight
   193  	}
   194  
   195  	if root == nil {
   196  		return
   197  	}
   198  	root.SetRect(image.Rect(0, 0, outsideWidth, outsideHeight))
   199  }
   200  
   201  // Update handles user input and passes it to the focused or clicked widget.
   202  func Update() error {
   203  	if root == nil {
   204  		return nil
   205  	}
   206  
   207  	var cursor image.Point
   208  
   209  	// Handle touch input.
   210  
   211  	var pressed bool
   212  	var clicked bool
   213  	var touchInput bool
   214  
   215  	if activeTouchID != -1 {
   216  		x, y := ebiten.TouchPosition(activeTouchID)
   217  		if x != 0 || y != 0 {
   218  			cursor = image.Point{x, y}
   219  
   220  			pressed = true
   221  			touchInput = true
   222  		} else {
   223  			activeTouchID = -1
   224  		}
   225  	}
   226  
   227  	if activeTouchID == -1 {
   228  		touchIDs = inpututil.AppendJustPressedTouchIDs(touchIDs[:0])
   229  		for _, id := range touchIDs {
   230  			x, y := ebiten.TouchPosition(id)
   231  			if x != 0 || y != 0 {
   232  				cursor = image.Point{x, y}
   233  
   234  				pressed = true
   235  				clicked = true
   236  				touchInput = true
   237  
   238  				activeTouchID = id
   239  				break
   240  			}
   241  		}
   242  	}
   243  
   244  	// Handle mouse input.
   245  
   246  	if !touchInput {
   247  		x, y := ebiten.CursorPosition()
   248  		cursor = image.Point{x, y}
   249  
   250  		if lastX == -math.MaxInt && lastY == -math.MaxInt {
   251  			lastX, lastY = x, y
   252  		}
   253  		for _, binding := range Bindings.ConfirmMouse {
   254  			pressed = ebiten.IsMouseButtonPressed(binding)
   255  			if pressed {
   256  				break
   257  			}
   258  		}
   259  
   260  		for _, binding := range Bindings.ConfirmMouse {
   261  			clicked = inpututil.IsMouseButtonJustPressed(binding)
   262  			if clicked {
   263  				break
   264  			}
   265  		}
   266  	}
   267  
   268  	if !pressed && !clicked && pressedWidget != nil {
   269  		_, err := pressedWidget.HandleMouse(cursor, false, false)
   270  		if err != nil {
   271  			return err
   272  		}
   273  		pressedWidget = nil
   274  	}
   275  
   276  	mouseHandled, err := update(root, cursor, pressed, clicked, false)
   277  	if err != nil {
   278  		return fmt.Errorf("failed to handle widget mouse input: %s", err)
   279  	} else if !mouseHandled && cursorShape != ebiten.CursorShapeDefault {
   280  		ebiten.SetCursorShape(ebiten.CursorShapeDefault)
   281  		cursorShape = ebiten.CursorShapeDefault
   282  	}
   283  
   284  	// Handle keyboard input.
   285  
   286  	if focusedWidget == nil {
   287  		return nil
   288  	} else if ebiten.IsKeyPressed(ebiten.KeyBackspace) {
   289  		if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) {
   290  			lastBackspaceRepeat = time.Now().Add(backspaceRepeatWait)
   291  		} else if time.Since(lastBackspaceRepeat) >= backspaceRepeatTime {
   292  			lastBackspaceRepeat = time.Now()
   293  
   294  			_, err := focusedWidget.HandleKeyboard(ebiten.KeyBackspace, 0)
   295  			if err != nil {
   296  				return err
   297  			}
   298  		}
   299  	}
   300  
   301  	keyBuffer = inpututil.AppendJustPressedKeys(keyBuffer[:0])
   302  	for _, key := range keyBuffer {
   303  		_, err := focusedWidget.HandleKeyboard(key, 0)
   304  		if err != nil {
   305  			return fmt.Errorf("failed to handle widget keyboard input: %s", err)
   306  		}
   307  	}
   308  
   309  	runeBuffer = ebiten.AppendInputChars(runeBuffer[:0])
   310  INPUTCHARS:
   311  	for i, r := range runeBuffer {
   312  		if i > 0 {
   313  			for j, r2 := range runeBuffer {
   314  				if j == i {
   315  					break
   316  				} else if r2 == r {
   317  					continue INPUTCHARS
   318  				}
   319  			}
   320  		}
   321  		var err error
   322  		switch r {
   323  		case Bindings.ConfirmRune:
   324  			_, err = focusedWidget.HandleKeyboard(ebiten.KeyEnter, 0)
   325  		case Bindings.BackRune:
   326  			_, err = focusedWidget.HandleKeyboard(ebiten.KeyBackspace, 0)
   327  		default:
   328  			_, err = focusedWidget.HandleKeyboard(-1, r)
   329  		}
   330  		if err != nil {
   331  			return fmt.Errorf("failed to handle widget keyboard input: %s", err)
   332  		}
   333  	}
   334  	return nil
   335  }
   336  
   337  func at(w Widget, p image.Point) Widget {
   338  	if w == nil || !w.Visible() {
   339  		return nil
   340  	}
   341  
   342  	for _, child := range w.Children() {
   343  		result := at(child, p)
   344  		if result != nil {
   345  			return result
   346  		}
   347  	}
   348  
   349  	if p.In(w.Rect()) {
   350  		return w
   351  	}
   352  
   353  	return nil
   354  }
   355  
   356  // At returns the widget at the provided screen location.
   357  func At(p image.Point) Widget {
   358  	return at(root, p)
   359  }
   360  
   361  func update(w Widget, cursor image.Point, pressed bool, clicked bool, mouseHandled bool) (bool, error) {
   362  	if w == nil {
   363  		return false, nil
   364  	}
   365  
   366  	if !w.Visible() {
   367  		return mouseHandled, nil
   368  	}
   369  
   370  	var err error
   371  	children := w.Children()
   372  	for i := len(children) - 1; i >= 0; i-- {
   373  		mouseHandled, err = update(children[i], cursor, pressed, clicked, mouseHandled)
   374  		if err != nil {
   375  			return false, err
   376  		} else if mouseHandled {
   377  			return true, nil
   378  		}
   379  	}
   380  	if !mouseHandled && cursor.In(w.Rect()) {
   381  		if pressed && !clicked && w != pressedWidget {
   382  			return mouseHandled, nil
   383  		}
   384  		mouseHandled, err = w.HandleMouse(cursor, pressed, clicked)
   385  		if err != nil {
   386  			return false, fmt.Errorf("failed to handle widget mouse input: %s", err)
   387  		} else if mouseHandled {
   388  			if clicked {
   389  				SetFocus(w)
   390  				pressedWidget = w
   391  			} else if pressedWidget != nil && (!pressed || pressedWidget != w) {
   392  				pressedWidget = nil
   393  			}
   394  			shape := w.Cursor()
   395  			if shape != -1 && shape != cursorShape {
   396  				ebiten.SetCursorShape(shape)
   397  				cursorShape = shape
   398  			}
   399  		}
   400  	}
   401  	return mouseHandled, nil
   402  }
   403  
   404  // Draw draws the root widget and its children to the screen.
   405  func Draw(screen *ebiten.Image) error {
   406  	return draw(root, screen)
   407  }
   408  
   409  func draw(w Widget, screen *ebiten.Image) error {
   410  	if w == nil {
   411  		return nil
   412  	}
   413  
   414  	if !w.Visible() {
   415  		return nil
   416  	}
   417  
   418  	background := w.Background()
   419  	if background.A > 0 {
   420  		screen.SubImage(w.Rect()).(*ebiten.Image).Fill(background)
   421  	}
   422  
   423  	err := w.Draw(screen)
   424  	if err != nil {
   425  		return fmt.Errorf("failed to draw widget: %s", err)
   426  	}
   427  
   428  	if drawDebug {
   429  		r := w.Rect()
   430  		if !r.Empty() {
   431  			x, y := r.Min.X, r.Min.Y
   432  			w, h := r.Dx(), r.Dy()
   433  			screen.SubImage(image.Rect(x, y, x+w, y+1)).(*ebiten.Image).Fill(debugColor)
   434  			screen.SubImage(image.Rect(x, y+h-1, x+w, y+h)).(*ebiten.Image).Fill(debugColor)
   435  			screen.SubImage(image.Rect(x, y, x+1, y+h)).(*ebiten.Image).Fill(debugColor)
   436  			screen.SubImage(image.Rect(x+w-1, y, x+w, y+h)).(*ebiten.Image).Fill(debugColor)
   437  		}
   438  	}
   439  
   440  	children := w.Children()
   441  	for _, child := range children {
   442  		err = draw(child, screen)
   443  		if err != nil {
   444  			return fmt.Errorf("failed to draw widget: %s", err)
   445  		}
   446  	}
   447  
   448  	return nil
   449  }
   450  
   451  func newText() *messeji.TextField {
   452  	f := messeji.NewTextField(FontFace(Style.TextFont, Scale(Style.TextSize)), fontMutex)
   453  	f.SetForegroundColor(Style.TextColorLight)
   454  	f.SetBackgroundColor(transparent)
   455  	f.SetScrollBarColors(Style.ScrollAreaColor, Style.ScrollHandleColor)
   456  	f.SetScrollBorderSize(Scale(Style.ScrollBorderSize))
   457  	f.SetScrollBorderColors(Style.ScrollBorderColorTop, Style.ScrollBorderColorRight, Style.ScrollBorderColorBottom, Style.ScrollBorderColorLeft)
   458  	return f
   459  }
   460  
   461  func rectAtOrigin(r image.Rectangle) image.Rectangle {
   462  	r.Max.X, r.Max.Y = r.Dx(), r.Dy()
   463  	r.Min.X, r.Min.Y = 0, 0
   464  	return r
   465  }
   466  

View as plain text