...

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

View as plain text