ui_js.go (12109B)
1 // Copyright 2015 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 package js 16 17 import ( 18 "syscall/js" 19 "time" 20 21 "github.com/hajimehoshi/ebiten/v2/internal/devicescale" 22 "github.com/hajimehoshi/ebiten/v2/internal/driver" 23 "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/opengl" 24 "github.com/hajimehoshi/ebiten/v2/internal/hooks" 25 "github.com/hajimehoshi/ebiten/v2/internal/jsutil" 26 "github.com/hajimehoshi/ebiten/v2/internal/restorable" 27 ) 28 29 type UserInterface struct { 30 runnableOnUnfocused bool 31 vsync bool 32 running bool 33 initFocused bool 34 35 sizeChanged bool 36 contextLost bool 37 38 lastDeviceScaleFactor float64 39 40 context driver.UIContext 41 input Input 42 } 43 44 var theUI = &UserInterface{ 45 runnableOnUnfocused: true, 46 sizeChanged: true, 47 vsync: true, 48 initFocused: true, 49 } 50 51 func init() { 52 theUI.input.ui = theUI 53 } 54 55 func Get() *UserInterface { 56 return theUI 57 } 58 59 var ( 60 window = js.Global().Get("window") 61 document = js.Global().Get("document") 62 canvas js.Value 63 requestAnimationFrame = window.Get("requestAnimationFrame") 64 setTimeout = window.Get("setTimeout") 65 ) 66 67 func (u *UserInterface) ScreenSizeInFullscreen() (int, int) { 68 return window.Get("innerWidth").Int(), window.Get("innerHeight").Int() 69 } 70 71 func (u *UserInterface) SetFullscreen(fullscreen bool) { 72 // Do nothing 73 } 74 75 func (u *UserInterface) IsFullscreen() bool { 76 return false 77 } 78 79 func (u *UserInterface) IsFocused() bool { 80 return u.isFocused() 81 } 82 83 func (u *UserInterface) SetRunnableOnUnfocused(runnableOnUnfocused bool) { 84 u.runnableOnUnfocused = runnableOnUnfocused 85 } 86 87 func (u *UserInterface) IsRunnableOnUnfocused() bool { 88 return u.runnableOnUnfocused 89 } 90 91 func (u *UserInterface) SetVsyncEnabled(enabled bool) { 92 u.vsync = enabled 93 } 94 95 func (u *UserInterface) IsVsyncEnabled() bool { 96 return u.vsync 97 } 98 99 func (u *UserInterface) CursorMode() driver.CursorMode { 100 if canvas.Get("style").Get("cursor").String() != "none" { 101 return driver.CursorModeVisible 102 } 103 return driver.CursorModeHidden 104 } 105 106 func (u *UserInterface) SetCursorMode(mode driver.CursorMode) { 107 var visible bool 108 switch mode { 109 case driver.CursorModeVisible: 110 visible = true 111 case driver.CursorModeHidden: 112 visible = false 113 default: 114 return 115 } 116 117 if visible { 118 canvas.Get("style").Set("cursor", "auto") 119 } else { 120 canvas.Get("style").Set("cursor", "none") 121 } 122 } 123 124 func (u *UserInterface) DeviceScaleFactor() float64 { 125 return devicescale.GetAt(0, 0) 126 } 127 128 func (u *UserInterface) updateSize() { 129 a := u.DeviceScaleFactor() 130 if u.lastDeviceScaleFactor != a { 131 u.updateScreenSize() 132 } 133 u.lastDeviceScaleFactor = a 134 135 if u.sizeChanged { 136 u.sizeChanged = false 137 body := document.Get("body") 138 bw := body.Get("clientWidth").Float() 139 bh := body.Get("clientHeight").Float() 140 u.context.Layout(bw, bh) 141 } 142 } 143 144 func (u *UserInterface) suspended() bool { 145 if u.runnableOnUnfocused { 146 return false 147 } 148 return !u.isFocused() 149 } 150 151 func (u *UserInterface) isFocused() bool { 152 if !document.Call("hasFocus").Bool() { 153 return false 154 } 155 if document.Get("hidden").Bool() { 156 return false 157 } 158 return true 159 } 160 161 func (u *UserInterface) update() error { 162 if u.suspended() { 163 hooks.SuspendAudio() 164 return nil 165 } 166 hooks.ResumeAudio() 167 168 u.input.UpdateGamepads() 169 u.updateSize() 170 if err := u.context.Update(); err != nil { 171 return err 172 } 173 if err := u.context.Draw(); err != nil { 174 return err 175 } 176 return nil 177 } 178 179 func (u *UserInterface) loop(context driver.UIContext) <-chan error { 180 u.context = context 181 182 errCh := make(chan error) 183 reqStopAudioCh := make(chan struct{}) 184 resStopAudioCh := make(chan struct{}) 185 186 var cf js.Func 187 f := func() { 188 if u.contextLost { 189 requestAnimationFrame.Invoke(cf) 190 return 191 } 192 193 if err := u.update(); err != nil { 194 close(reqStopAudioCh) 195 <-resStopAudioCh 196 197 errCh <- err 198 close(errCh) 199 return 200 } 201 if u.vsync { 202 requestAnimationFrame.Invoke(cf) 203 } else { 204 setTimeout.Invoke(cf, 0) 205 } 206 } 207 208 // TODO: Should cf be released after the game ends? 209 cf = js.FuncOf(func(this js.Value, args []js.Value) interface{} { 210 // f can be blocked but callbacks must not be blocked. Create a goroutine (#1161). 211 go f() 212 return nil 213 }) 214 215 // Call f asyncly to be async since ch is used in f. 216 go f() 217 218 // Run another loop to watch suspended() as the above update function is never called when the tab is hidden. 219 // To check the document's visiblity, visibilitychange event should usually be used. However, this event is 220 // not reliable and sometimes it is not fired (#961). Then, watch the state regularly instead. 221 go func() { 222 defer close(resStopAudioCh) 223 224 const interval = 100 * time.Millisecond 225 t := time.NewTicker(interval) 226 defer func() { 227 t.Stop() 228 229 // This is a dirty hack. (*time.Ticker).Stop() just marks the timer 'deleted' [1] and 230 // something might run even after Stop. On Wasm, this causes an issue to execute Go program 231 // even after finishing (#1027). Sleep for the interval time duration to ensure that 232 // everything related to the timer is finished. 233 // 234 // [1] runtime.deltimer 235 time.Sleep(interval) 236 }() 237 238 for { 239 select { 240 case <-t.C: 241 if u.suspended() { 242 hooks.SuspendAudio() 243 } else { 244 hooks.ResumeAudio() 245 } 246 case <-reqStopAudioCh: 247 return 248 } 249 } 250 }() 251 252 return errCh 253 } 254 255 func init() { 256 if jsutil.Equal(document.Get("body"), js.Null()) { 257 ch := make(chan struct{}) 258 window.Call("addEventListener", "load", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 259 close(ch) 260 return nil 261 })) 262 <-ch 263 } 264 265 window.Call("addEventListener", "resize", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 266 theUI.updateScreenSize() 267 return nil 268 })) 269 270 // Adjust the initial scale to 1. 271 // https://developer.mozilla.org/en/docs/Mozilla/Mobile/Viewport_meta_tag 272 meta := document.Call("createElement", "meta") 273 meta.Set("name", "viewport") 274 meta.Set("content", "width=device-width, initial-scale=1") 275 document.Get("head").Call("appendChild", meta) 276 277 canvas = document.Call("createElement", "canvas") 278 canvas.Set("width", 16) 279 canvas.Set("height", 16) 280 281 document.Get("body").Call("appendChild", canvas) 282 283 htmlStyle := document.Get("documentElement").Get("style") 284 htmlStyle.Set("height", "100%") 285 htmlStyle.Set("margin", "0") 286 htmlStyle.Set("padding", "0") 287 288 bodyStyle := document.Get("body").Get("style") 289 bodyStyle.Set("backgroundColor", "#000") 290 bodyStyle.Set("height", "100%") 291 bodyStyle.Set("margin", "0") 292 bodyStyle.Set("padding", "0") 293 294 canvasStyle := canvas.Get("style") 295 canvasStyle.Set("width", "100%") 296 canvasStyle.Set("height", "100%") 297 canvasStyle.Set("margin", "0") 298 canvasStyle.Set("padding", "0") 299 300 // Make the canvas focusable. 301 canvas.Call("setAttribute", "tabindex", 1) 302 canvas.Get("style").Set("outline", "none") 303 304 // Keyboard 305 canvas.Call("addEventListener", "keydown", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 306 // Focus the canvas explicitly to activate tha game (#961). 307 canvas.Call("focus") 308 309 e := args[0] 310 // Don't 'preventDefault' on keydown events or keypress events wouldn't work (#715). 311 theUI.input.Update(e) 312 return nil 313 })) 314 canvas.Call("addEventListener", "keypress", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 315 e := args[0] 316 e.Call("preventDefault") 317 theUI.input.Update(e) 318 return nil 319 })) 320 canvas.Call("addEventListener", "keyup", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 321 e := args[0] 322 e.Call("preventDefault") 323 theUI.input.Update(e) 324 return nil 325 })) 326 327 // Mouse 328 canvas.Call("addEventListener", "mousedown", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 329 // Focus the canvas explicitly to activate tha game (#961). 330 canvas.Call("focus") 331 332 e := args[0] 333 e.Call("preventDefault") 334 theUI.input.Update(e) 335 return nil 336 })) 337 canvas.Call("addEventListener", "mouseup", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 338 e := args[0] 339 e.Call("preventDefault") 340 theUI.input.Update(e) 341 return nil 342 })) 343 canvas.Call("addEventListener", "mousemove", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 344 e := args[0] 345 e.Call("preventDefault") 346 theUI.input.Update(e) 347 return nil 348 })) 349 canvas.Call("addEventListener", "wheel", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 350 e := args[0] 351 e.Call("preventDefault") 352 theUI.input.Update(e) 353 return nil 354 })) 355 356 // Touch 357 canvas.Call("addEventListener", "touchstart", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 358 // Focus the canvas explicitly to activate tha game (#961). 359 canvas.Call("focus") 360 361 e := args[0] 362 e.Call("preventDefault") 363 theUI.input.Update(e) 364 return nil 365 })) 366 canvas.Call("addEventListener", "touchend", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 367 e := args[0] 368 e.Call("preventDefault") 369 theUI.input.Update(e) 370 return nil 371 })) 372 canvas.Call("addEventListener", "touchmove", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 373 e := args[0] 374 e.Call("preventDefault") 375 theUI.input.Update(e) 376 return nil 377 })) 378 379 // Gamepad 380 window.Call("addEventListener", "gamepadconnected", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 381 // Do nothing. 382 return nil 383 })) 384 385 canvas.Call("addEventListener", "contextmenu", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 386 e := args[0] 387 e.Call("preventDefault") 388 return nil 389 })) 390 391 // Context 392 canvas.Call("addEventListener", "webglcontextlost", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 393 e := args[0] 394 e.Call("preventDefault") 395 theUI.contextLost = true 396 restorable.OnContextLost() 397 return nil 398 })) 399 canvas.Call("addEventListener", "webglcontextrestored", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 400 theUI.contextLost = false 401 return nil 402 })) 403 } 404 405 func (u *UserInterface) Run(context driver.UIContext) error { 406 if u.initFocused { 407 // Do not focus the canvas when the current document is in an iframe. 408 // Otherwise, the parent page tries to focus the iframe on every loading, which is annoying (#1373). 409 isInIframe := !jsutil.Equal(window.Get("location"), window.Get("parent").Get("location")) 410 if !isInIframe { 411 canvas.Call("focus") 412 } 413 } 414 u.running = true 415 return <-u.loop(context) 416 } 417 418 func (u *UserInterface) RunWithoutMainLoop(context driver.UIContext) { 419 panic("js: RunWithoutMainLoop is not implemented") 420 } 421 422 func (u *UserInterface) updateScreenSize() { 423 body := document.Get("body") 424 bw := int(body.Get("clientWidth").Float() * u.DeviceScaleFactor()) 425 bh := int(body.Get("clientHeight").Float() * u.DeviceScaleFactor()) 426 canvas.Set("width", bw) 427 canvas.Set("height", bh) 428 u.sizeChanged = true 429 } 430 431 func (u *UserInterface) SetScreenTransparent(transparent bool) { 432 if u.running { 433 panic("js: SetScreenTransparent can't be called after the main loop starts") 434 } 435 436 bodyStyle := document.Get("body").Get("style") 437 if transparent { 438 bodyStyle.Set("backgroundColor", "transparent") 439 } else { 440 bodyStyle.Set("backgroundColor", "#000") 441 } 442 } 443 444 func (u *UserInterface) IsScreenTransparent() bool { 445 bodyStyle := document.Get("body").Get("style") 446 return bodyStyle.Get("backgroundColor").String() == "transparent" 447 } 448 449 func (u *UserInterface) ResetForFrame() { 450 u.updateSize() 451 u.input.resetForFrame() 452 } 453 454 func (u *UserInterface) SetInitFocused(focused bool) { 455 if u.running { 456 panic("ui: SetInitFocused must be called before the main loop") 457 } 458 u.initFocused = focused 459 } 460 461 func (u *UserInterface) Input() driver.Input { 462 return &u.input 463 } 464 465 func (u *UserInterface) Window() driver.Window { 466 return nil 467 } 468 469 func (*UserInterface) Graphics() driver.Graphics { 470 return opengl.Get() 471 }