...

Source file src/codeberg.org/tslocum/etk/game.go

Documentation: codeberg.org/tslocum/etk

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

View as plain text