...

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