zorldo

Goofing around with Ebiten
git clone git://bsandro.tech/zorldo
Log | Files | Refs | README

ui.go (11231B)


      1 // Copyright 2016 Hajime Hoshi
      2 //
      3 // Licensed under the Apache License, Version 2.0 (the "License");
      4 // you may not use this file except in compliance with the License.
      5 // You may obtain a copy of the License at
      6 //
      7 //     http://www.apache.org/licenses/LICENSE-2.0
      8 //
      9 // Unless required by applicable law or agreed to in writing, software
     10 // distributed under the License is distributed on an "AS IS" BASIS,
     11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 // See the License for the specific language governing permissions and
     13 // limitations under the License.
     14 
     15 //go:build android || ios
     16 // +build android ios
     17 
     18 package mobile
     19 
     20 import (
     21 	"fmt"
     22 	"runtime/debug"
     23 	"sync"
     24 	"sync/atomic"
     25 	"unicode"
     26 
     27 	"golang.org/x/mobile/app"
     28 	"golang.org/x/mobile/event/key"
     29 	"golang.org/x/mobile/event/lifecycle"
     30 	"golang.org/x/mobile/event/paint"
     31 	"golang.org/x/mobile/event/size"
     32 	"golang.org/x/mobile/event/touch"
     33 	"golang.org/x/mobile/gl"
     34 
     35 	"github.com/hajimehoshi/ebiten/v2/internal/devicescale"
     36 	"github.com/hajimehoshi/ebiten/v2/internal/driver"
     37 	"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
     38 	"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/opengl"
     39 	"github.com/hajimehoshi/ebiten/v2/internal/hooks"
     40 	"github.com/hajimehoshi/ebiten/v2/internal/restorable"
     41 	"github.com/hajimehoshi/ebiten/v2/internal/thread"
     42 )
     43 
     44 var (
     45 	glContextCh = make(chan gl.Context, 1)
     46 
     47 	// renderCh receives when updating starts.
     48 	renderCh = make(chan struct{})
     49 
     50 	// renderEndCh receives when updating finishes.
     51 	renderEndCh = make(chan struct{})
     52 
     53 	theUI = &UserInterface{
     54 		foreground: 1,
     55 		errCh:      make(chan error),
     56 
     57 		// Give a default outside size so that the game can start without initializing them.
     58 		outsideWidth:  640,
     59 		outsideHeight: 480,
     60 		sizeChanged:   true,
     61 	}
     62 )
     63 
     64 func init() {
     65 	theUI.input.ui = theUI
     66 }
     67 
     68 func Get() *UserInterface {
     69 	return theUI
     70 }
     71 
     72 // Update is called from mobile/ebitenmobileview.
     73 //
     74 // Update must be called on the rendering thread.
     75 func (u *UserInterface) Update() error {
     76 	select {
     77 	case err := <-u.errCh:
     78 		return err
     79 	default:
     80 	}
     81 
     82 	if !u.IsFocused() {
     83 		return nil
     84 	}
     85 
     86 	renderCh <- struct{}{}
     87 	go func() {
     88 		<-renderEndCh
     89 		u.t.Call(func() error {
     90 			return thread.BreakLoop
     91 		})
     92 	}()
     93 	u.t.Loop()
     94 	return nil
     95 }
     96 
     97 type UserInterface struct {
     98 	outsideWidth  float64
     99 	outsideHeight float64
    100 
    101 	sizeChanged bool
    102 	foreground  int32
    103 	errCh       chan error
    104 
    105 	// Used for gomobile-build
    106 	gbuildWidthPx   int
    107 	gbuildHeightPx  int
    108 	setGBuildSizeCh chan struct{}
    109 	once            sync.Once
    110 
    111 	context driver.UIContext
    112 
    113 	input Input
    114 
    115 	fpsMode         driver.FPSMode
    116 	renderRequester RenderRequester
    117 
    118 	t *thread.OSThread
    119 
    120 	m sync.RWMutex
    121 }
    122 
    123 func deviceScale() float64 {
    124 	return devicescale.GetAt(0, 0)
    125 }
    126 
    127 // appMain is the main routine for gomobile-build mode.
    128 func (u *UserInterface) appMain(a app.App) {
    129 	var glctx gl.Context
    130 	var sizeInited bool
    131 
    132 	touches := map[touch.Sequence]*Touch{}
    133 	keys := map[driver.Key]struct{}{}
    134 
    135 	for e := range a.Events() {
    136 		var updateInput bool
    137 		var runes []rune
    138 
    139 		switch e := a.Filter(e).(type) {
    140 		case lifecycle.Event:
    141 			switch e.Crosses(lifecycle.StageVisible) {
    142 			case lifecycle.CrossOn:
    143 				if err := u.SetForeground(true); err != nil {
    144 					// There are no other ways than panicking here.
    145 					panic(err)
    146 				}
    147 				restorable.OnContextLost()
    148 				glctx, _ = e.DrawContext.(gl.Context)
    149 				// Assume that glctx is always a same instance.
    150 				// Then, only once initializing should be enough.
    151 				if glContextCh != nil {
    152 					glContextCh <- glctx
    153 					glContextCh = nil
    154 				}
    155 				a.Send(paint.Event{})
    156 			case lifecycle.CrossOff:
    157 				if err := u.SetForeground(false); err != nil {
    158 					// There are no other ways than panicking here.
    159 					panic(err)
    160 				}
    161 				glctx = nil
    162 			}
    163 		case size.Event:
    164 			u.setGBuildSize(e.WidthPx, e.HeightPx)
    165 			sizeInited = true
    166 		case paint.Event:
    167 			if !sizeInited {
    168 				a.Send(paint.Event{})
    169 				continue
    170 			}
    171 			if glctx == nil || e.External {
    172 				continue
    173 			}
    174 			renderCh <- struct{}{}
    175 			<-renderEndCh
    176 			a.Publish()
    177 			a.Send(paint.Event{})
    178 		case touch.Event:
    179 			if !sizeInited {
    180 				continue
    181 			}
    182 			switch e.Type {
    183 			case touch.TypeBegin, touch.TypeMove:
    184 				s := deviceScale()
    185 				x, y := float64(e.X)/s, float64(e.Y)/s
    186 				// TODO: Is it ok to cast from int64 to int here?
    187 				touches[e.Sequence] = &Touch{
    188 					ID: driver.TouchID(e.Sequence),
    189 					X:  int(x),
    190 					Y:  int(y),
    191 				}
    192 			case touch.TypeEnd:
    193 				delete(touches, e.Sequence)
    194 			}
    195 			updateInput = true
    196 		case key.Event:
    197 			k, ok := gbuildKeyToDriverKey[e.Code]
    198 			if ok {
    199 				switch e.Direction {
    200 				case key.DirPress, key.DirNone:
    201 					keys[k] = struct{}{}
    202 				case key.DirRelease:
    203 					delete(keys, k)
    204 				}
    205 			}
    206 
    207 			switch e.Direction {
    208 			case key.DirPress, key.DirNone:
    209 				if e.Rune != -1 && unicode.IsPrint(e.Rune) {
    210 					runes = []rune{e.Rune}
    211 				}
    212 			}
    213 			updateInput = true
    214 		}
    215 
    216 		if updateInput {
    217 			ts := []*Touch{}
    218 			for _, t := range touches {
    219 				ts = append(ts, t)
    220 			}
    221 			u.input.update(keys, runes, ts, nil)
    222 		}
    223 	}
    224 }
    225 
    226 func (u *UserInterface) SetForeground(foreground bool) error {
    227 	var v int32
    228 	if foreground {
    229 		v = 1
    230 	}
    231 	atomic.StoreInt32(&u.foreground, v)
    232 
    233 	if foreground {
    234 		return hooks.ResumeAudio()
    235 	} else {
    236 		return hooks.SuspendAudio()
    237 	}
    238 }
    239 
    240 func (u *UserInterface) Run(context driver.UIContext) error {
    241 	u.setGBuildSizeCh = make(chan struct{})
    242 	go func() {
    243 		if err := u.run(context, true); err != nil {
    244 			// As mobile apps never ends, Loop can't return. Just panic here.
    245 			panic(err)
    246 		}
    247 	}()
    248 	app.Main(u.appMain)
    249 	return nil
    250 }
    251 
    252 func (u *UserInterface) RunWithoutMainLoop(context driver.UIContext) {
    253 	go func() {
    254 		if err := u.run(context, false); err != nil {
    255 			u.errCh <- err
    256 		}
    257 	}()
    258 }
    259 
    260 func (u *UserInterface) run(context driver.UIContext, mainloop bool) (err error) {
    261 	// Convert the panic to a regular error so that Java/Objective-C layer can treat this easily e.g., for
    262 	// Crashlytics. A panic is treated as SIGABRT, and there is no way to handle this on Java/Objective-C layer
    263 	// unfortunately.
    264 	// TODO: Panic on other goroutines cannot be handled here.
    265 	defer func() {
    266 		if r := recover(); r != nil {
    267 			err = fmt.Errorf("%v\n%s", r, string(debug.Stack()))
    268 		}
    269 	}()
    270 
    271 	u.m.Lock()
    272 	u.sizeChanged = true
    273 	u.m.Unlock()
    274 
    275 	u.context = context
    276 
    277 	if mainloop {
    278 		// When mainloop is true, gomobile-build is used. In this case, GL functions must be called via
    279 		// gl.Context so that they are called on the appropriate thread.
    280 		ctx := <-glContextCh
    281 		u.Graphics().(*opengl.Graphics).SetGomobileGLContext(ctx)
    282 	} else {
    283 		u.t = thread.NewOSThread()
    284 		graphicscommand.SetMainThread(u.t)
    285 	}
    286 
    287 	// If gomobile-build is used, wait for the outside size fixed.
    288 	if u.setGBuildSizeCh != nil {
    289 		<-u.setGBuildSizeCh
    290 	}
    291 
    292 	// Force to set the screen size
    293 	u.layoutIfNeeded()
    294 	for {
    295 		if err := u.update(); err != nil {
    296 			return err
    297 		}
    298 	}
    299 }
    300 
    301 // layoutIfNeeded must be called on the same goroutine as update().
    302 func (u *UserInterface) layoutIfNeeded() {
    303 	var outsideWidth, outsideHeight float64
    304 
    305 	u.m.RLock()
    306 	sizeChanged := u.sizeChanged
    307 	if sizeChanged {
    308 		if u.gbuildWidthPx == 0 || u.gbuildHeightPx == 0 {
    309 			outsideWidth = u.outsideWidth
    310 			outsideHeight = u.outsideHeight
    311 		} else {
    312 			// gomobile build
    313 			d := deviceScale()
    314 			outsideWidth = float64(u.gbuildWidthPx) / d
    315 			outsideHeight = float64(u.gbuildHeightPx) / d
    316 		}
    317 	}
    318 	u.sizeChanged = false
    319 	u.m.RUnlock()
    320 
    321 	if sizeChanged {
    322 		u.context.Layout(outsideWidth, outsideHeight)
    323 	}
    324 }
    325 
    326 func (u *UserInterface) update() error {
    327 	<-renderCh
    328 	defer func() {
    329 		renderEndCh <- struct{}{}
    330 	}()
    331 
    332 	if err := u.context.UpdateFrame(); err != nil {
    333 		return err
    334 	}
    335 	return nil
    336 }
    337 
    338 func (u *UserInterface) ScreenSizeInFullscreen() (int, int) {
    339 	// TODO: This function should return gbuildWidthPx, gbuildHeightPx,
    340 	// but these values are not initialized until the main loop starts.
    341 	return 0, 0
    342 }
    343 
    344 // SetOutsideSize is called from mobile/ebitenmobileview.
    345 //
    346 // SetOutsideSize is concurrent safe.
    347 func (u *UserInterface) SetOutsideSize(outsideWidth, outsideHeight float64) {
    348 	u.m.Lock()
    349 	if u.outsideWidth != outsideWidth || u.outsideHeight != outsideHeight {
    350 		u.outsideWidth = outsideWidth
    351 		u.outsideHeight = outsideHeight
    352 		u.sizeChanged = true
    353 	}
    354 	u.m.Unlock()
    355 }
    356 
    357 func (u *UserInterface) setGBuildSize(widthPx, heightPx int) {
    358 	u.m.Lock()
    359 	u.gbuildWidthPx = widthPx
    360 	u.gbuildHeightPx = heightPx
    361 	u.sizeChanged = true
    362 	u.m.Unlock()
    363 
    364 	u.once.Do(func() {
    365 		close(u.setGBuildSizeCh)
    366 	})
    367 }
    368 
    369 func (u *UserInterface) adjustPosition(x, y int) (int, int) {
    370 	xf, yf := u.context.AdjustPosition(float64(x), float64(y), deviceScale())
    371 	return int(xf), int(yf)
    372 }
    373 
    374 func (u *UserInterface) CursorMode() driver.CursorMode {
    375 	return driver.CursorModeHidden
    376 }
    377 
    378 func (u *UserInterface) SetCursorMode(mode driver.CursorMode) {
    379 	// Do nothing
    380 }
    381 
    382 func (u *UserInterface) CursorShape() driver.CursorShape {
    383 	return driver.CursorShapeDefault
    384 }
    385 
    386 func (u *UserInterface) SetCursorShape(shape driver.CursorShape) {
    387 	// Do nothing
    388 }
    389 
    390 func (u *UserInterface) IsFullscreen() bool {
    391 	return false
    392 }
    393 
    394 func (u *UserInterface) SetFullscreen(fullscreen bool) {
    395 	// Do nothing
    396 }
    397 
    398 func (u *UserInterface) IsFocused() bool {
    399 	return atomic.LoadInt32(&u.foreground) != 0
    400 }
    401 
    402 func (u *UserInterface) IsRunnableOnUnfocused() bool {
    403 	return false
    404 }
    405 
    406 func (u *UserInterface) SetRunnableOnUnfocused(runnableOnUnfocused bool) {
    407 	// Do nothing
    408 }
    409 
    410 func (u *UserInterface) FPSMode() driver.FPSMode {
    411 	return u.fpsMode
    412 }
    413 
    414 func (u *UserInterface) SetFPSMode(mode driver.FPSMode) {
    415 	u.fpsMode = mode
    416 	u.updateExplicitRenderingModeIfNeeded()
    417 }
    418 
    419 func (u *UserInterface) updateExplicitRenderingModeIfNeeded() {
    420 	if u.renderRequester == nil {
    421 		return
    422 	}
    423 	u.renderRequester.SetExplicitRenderingMode(u.fpsMode == driver.FPSModeVsyncOffMinimum)
    424 }
    425 
    426 func (u *UserInterface) DeviceScaleFactor() float64 {
    427 	return deviceScale()
    428 }
    429 
    430 func (u *UserInterface) SetScreenTransparent(transparent bool) {
    431 	// Do nothing
    432 }
    433 
    434 func (u *UserInterface) IsScreenTransparent() bool {
    435 	return false
    436 }
    437 
    438 func (u *UserInterface) ResetForFrame() {
    439 	u.layoutIfNeeded()
    440 	u.input.resetForFrame()
    441 }
    442 
    443 func (u *UserInterface) SetInitFocused(focused bool) {
    444 	// Do nothing
    445 }
    446 
    447 func (u *UserInterface) Input() driver.Input {
    448 	return &u.input
    449 }
    450 
    451 func (u *UserInterface) Window() driver.Window {
    452 	return nil
    453 }
    454 
    455 type Touch struct {
    456 	ID driver.TouchID
    457 	X  int
    458 	Y  int
    459 }
    460 
    461 type Gamepad struct {
    462 	ID        driver.GamepadID
    463 	SDLID     string
    464 	Name      string
    465 	Buttons   [driver.GamepadButtonNum]bool
    466 	ButtonNum int
    467 	Axes      [32]float32
    468 	AxisNum   int
    469 }
    470 
    471 func (u *UserInterface) UpdateInput(keys map[driver.Key]struct{}, runes []rune, touches []*Touch, gamepads []Gamepad) {
    472 	u.input.update(keys, runes, touches, gamepads)
    473 	if u.fpsMode == driver.FPSModeVsyncOffMinimum {
    474 		u.renderRequester.RequestRenderIfNeeded()
    475 	}
    476 }
    477 
    478 type RenderRequester interface {
    479 	SetExplicitRenderingMode(explicitRendering bool)
    480 	RequestRenderIfNeeded()
    481 }
    482 
    483 func (u *UserInterface) SetRenderRequester(renderRequester RenderRequester) {
    484 	u.renderRequester = renderRequester
    485 	u.updateExplicitRenderingModeIfNeeded()
    486 }
    487 
    488 func (u *UserInterface) ScheduleFrame() {
    489 	if u.renderRequester != nil && u.fpsMode == driver.FPSModeVsyncOffMinimum {
    490 		u.renderRequester.RequestRenderIfNeeded()
    491 	}
    492 }