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 }