...

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

View as plain text