2015-01-02 07:20:05 +01:00
// Copyright 2015 Hajime Hoshi
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
2022-02-06 08:08:22 +01:00
package ui
2015-01-02 07:20:05 +01:00
import (
2023-10-15 16:27:04 +02:00
"errors"
2023-08-30 14:02:04 +02:00
"image"
2023-09-17 09:52:21 +02:00
"math"
2022-09-20 06:19:40 +02:00
"sync"
2019-04-30 19:15:28 +02:00
"syscall/js"
2019-10-20 17:02:35 +02:00
"time"
2018-01-02 21:22:56 +01:00
2023-01-22 18:48:50 +01:00
"github.com/hajimehoshi/ebiten/v2/internal/file"
2022-02-05 14:21:41 +01:00
"github.com/hajimehoshi/ebiten/v2/internal/gamepad"
2022-03-21 16:02:37 +01:00
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
2022-03-22 15:33:21 +01:00
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/opengl"
2023-10-06 06:49:42 +02:00
"github.com/hajimehoshi/ebiten/v2/internal/hook"
2015-01-02 07:20:05 +01:00
)
2022-11-13 07:18:44 +01:00
type graphicsDriverCreatorImpl struct {
canvas js . Value
}
2022-03-22 15:33:21 +01:00
2022-07-30 19:56:16 +02:00
func ( g * graphicsDriverCreatorImpl ) newAuto ( ) ( graphicsdriver . Graphics , GraphicsLibrary , error ) {
graphics , err := g . newOpenGL ( )
return graphics , GraphicsLibraryOpenGL , err
2022-03-22 15:33:21 +01:00
}
2022-11-13 07:18:44 +01:00
func ( g * graphicsDriverCreatorImpl ) newOpenGL ( ) ( graphicsdriver . Graphics , error ) {
return opengl . NewGraphics ( g . canvas )
2022-03-22 15:33:21 +01:00
}
2022-06-17 04:39:43 +02:00
func ( * graphicsDriverCreatorImpl ) newDirectX ( ) ( graphicsdriver . Graphics , error ) {
2023-10-15 16:27:04 +02:00
return nil , errors . New ( "ui: DirectX is not supported in this environment" )
2022-03-25 11:43:32 +01:00
}
2022-06-16 19:10:29 +02:00
func ( * graphicsDriverCreatorImpl ) newMetal ( ) ( graphicsdriver . Graphics , error ) {
2023-10-15 16:27:04 +02:00
return nil , errors . New ( "ui: Metal is not supported in this environment" )
2022-03-22 15:33:21 +01:00
}
2023-10-01 16:14:47 +02:00
func ( * graphicsDriverCreatorImpl ) newPlayStation5 ( ) ( graphicsdriver . Graphics , error ) {
return nil , errors . New ( "ui: PlayStation 5 is not supported in this environment" )
}
2020-12-16 04:41:49 +01:00
var (
stringNone = js . ValueOf ( "none" )
stringTransparent = js . ValueOf ( "transparent" )
)
2022-02-06 09:43:52 +01:00
func driverCursorShapeToCSSCursor ( cursor CursorShape ) string {
2021-04-11 07:21:38 +02:00
switch cursor {
2022-02-06 09:43:52 +01:00
case CursorShapeDefault :
2021-04-11 07:21:38 +02:00
return "default"
2022-02-06 09:43:52 +01:00
case CursorShapeText :
2021-04-11 07:21:38 +02:00
return "text"
2022-02-06 09:43:52 +01:00
case CursorShapeCrosshair :
2021-04-11 07:21:38 +02:00
return "crosshair"
2022-02-06 09:43:52 +01:00
case CursorShapePointer :
2021-04-11 07:21:38 +02:00
return "pointer"
2022-02-06 09:43:52 +01:00
case CursorShapeEWResize :
2021-05-02 07:50:50 +02:00
return "ew-resize"
2022-02-06 09:43:52 +01:00
case CursorShapeNSResize :
2021-05-02 07:50:50 +02:00
return "ns-resize"
2023-01-15 18:26:41 +01:00
case CursorShapeNESWResize :
return "nesw-resize"
case CursorShapeNWSEResize :
return "nwse-resize"
case CursorShapeMove :
return "move"
case CursorShapeNotAllowed :
return "not-allowed"
2021-04-11 07:21:38 +02:00
}
return "auto"
}
2022-03-21 15:00:50 +01:00
type userInterfaceImpl struct {
2022-03-21 16:02:37 +01:00
graphicsDriver graphicsdriver . Graphics
2020-03-20 16:34:12 +01:00
runnableOnUnfocused bool
2022-02-13 09:23:52 +01:00
fpsMode FPSModeType
2021-07-22 18:07:28 +02:00
renderingScheduled bool
2020-03-20 16:34:12 +01:00
running bool
2022-02-06 09:43:52 +01:00
cursorMode CursorMode
cursorPrevMode CursorMode
2023-07-06 18:17:28 +02:00
captureCursorLater bool
2022-02-06 09:43:52 +01:00
cursorShape CursorShape
2021-07-22 18:07:28 +02:00
onceUpdateCalled bool
2023-07-06 18:17:28 +02:00
lastCaptureExitTime time . Time
2017-07-21 18:52:08 +02:00
2023-09-23 20:03:07 +02:00
deviceScaleFactor float64
2019-04-06 16:03:21 +02:00
2023-09-17 09:17:01 +02:00
context * context
inputState InputState
cursorXInClient float64
cursorYInClient float64
origCursorXInClient float64
origCursorYInClient float64
touchesInClient [ ] touchInClient
2022-09-20 06:19:40 +02:00
2023-09-17 09:52:21 +02:00
savedCursorX float64
savedCursorY float64
savedOutsideWidth float64
savedOutsideHeight float64
outsideSizeUnchangedCount int
2022-08-13 06:33:52 +02:00
keyboardLayoutMap js . Value
2023-01-22 09:43:56 +01:00
m sync . Mutex
dropFileM sync . Mutex
2016-03-24 17:15:47 +01:00
}
2016-03-24 16:38:30 +01:00
2018-06-18 20:40:37 +02:00
var (
2018-06-29 17:02:15 +02:00
window = js . Global ( ) . Get ( "window" )
document = js . Global ( ) . Get ( "document" )
2019-10-20 17:02:35 +02:00
canvas js . Value
2020-11-22 10:22:52 +01:00
requestAnimationFrame = js . Global ( ) . Get ( "requestAnimationFrame" )
setTimeout = js . Global ( ) . Get ( "setTimeout" )
2018-06-18 20:40:37 +02:00
)
2021-09-04 12:20:24 +02:00
var (
documentHasFocus = document . Get ( "hasFocus" ) . Call ( "bind" , document )
2023-10-14 17:15:20 +02:00
documentHidden = js . Global ( ) . Get ( "Object" ) . Call ( "getOwnPropertyDescriptor" , js . Global ( ) . Get ( "Document" ) . Get ( "prototype" ) , "hidden" ) . Get ( "get" ) . Call ( "bind" , document )
)
2021-09-04 12:20:24 +02:00
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) ScreenSizeInFullscreen ( ) ( int , int ) {
2018-06-18 20:40:37 +02:00
return window . Get ( "innerWidth" ) . Int ( ) , window . Get ( "innerHeight" ) . Int ( )
2018-05-04 09:09:55 +02:00
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) SetFullscreen ( fullscreen bool ) {
2021-04-27 16:58:32 +02:00
if ! canvas . Truthy ( ) {
return
}
if ! document . Truthy ( ) {
return
}
2022-04-09 11:23:56 +02:00
if fullscreen == u . IsFullscreen ( ) {
2021-07-22 19:15:41 +02:00
return
}
2023-09-17 09:52:21 +02:00
2023-10-14 17:06:07 +02:00
if u . cursorMode == CursorModeCaptured {
u . saveCursorPosition ( )
2023-09-17 09:52:21 +02:00
}
2021-04-27 16:58:32 +02:00
if fullscreen {
f := canvas . Get ( "requestFullscreen" )
if ! f . Truthy ( ) {
f = canvas . Get ( "webkitRequestFullscreen" )
}
f . Call ( "bind" , canvas ) . Invoke ( )
return
}
2023-09-17 09:52:21 +02:00
2021-04-27 16:58:32 +02:00
f := document . Get ( "exitFullscreen" )
if ! f . Truthy ( ) {
f = document . Get ( "webkitExitFullscreen" )
}
f . Call ( "bind" , document ) . Invoke ( )
2017-06-29 17:35:34 +02:00
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) IsFullscreen ( ) bool {
2021-04-27 16:58:32 +02:00
if ! document . Truthy ( ) {
return false
}
if ! document . Get ( "fullscreenElement" ) . Truthy ( ) && ! document . Get ( "webkitFullscreenElement" ) . Truthy ( ) {
return false
}
return true
2017-06-29 17:35:34 +02:00
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) IsFocused ( ) bool {
2020-03-20 16:08:40 +01:00
return u . isFocused ( )
2020-01-16 02:47:23 +01:00
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) SetRunnableOnUnfocused ( runnableOnUnfocused bool ) {
2020-03-20 16:34:12 +01:00
u . runnableOnUnfocused = runnableOnUnfocused
2017-08-02 16:37:50 +02:00
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) IsRunnableOnUnfocused ( ) bool {
2020-03-20 16:34:12 +01:00
return u . runnableOnUnfocused
2017-08-02 16:37:50 +02:00
}
2023-10-15 09:10:13 +02:00
func ( u * UserInterface ) FPSMode ( ) FPSModeType {
return u . fpsMode
}
func ( u * UserInterface ) SetFPSMode ( mode FPSModeType ) {
2021-07-22 16:55:26 +02:00
u . fpsMode = mode
2018-07-14 13:50:09 +02:00
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) ScheduleFrame ( ) {
2021-07-22 18:07:28 +02:00
u . renderingScheduled = true
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) CursorMode ( ) CursorMode {
2021-03-21 08:40:35 +01:00
if ! canvas . Truthy ( ) {
2022-02-06 09:43:52 +01:00
return CursorModeHidden
2021-03-21 08:40:35 +01:00
}
2021-04-15 19:26:10 +02:00
return u . cursorMode
2017-08-12 08:39:41 +02:00
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) SetCursorMode ( mode CursorMode ) {
2023-07-06 18:17:28 +02:00
if mode == CursorModeCaptured && ! u . canCaptureCursor ( ) {
u . captureCursorLater = true
return
}
u . setCursorMode ( mode )
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) setCursorMode ( mode CursorMode ) {
2023-07-06 18:17:28 +02:00
u . captureCursorLater = false
2021-03-21 08:40:35 +01:00
if ! canvas . Truthy ( ) {
return
}
2021-04-15 19:26:10 +02:00
if u . cursorMode == mode {
return
}
2021-04-15 20:14:46 +02:00
// Remember the previous cursor mode in the case when the pointer lock exits by pressing ESC.
2021-04-15 19:26:10 +02:00
u . cursorPrevMode = u . cursorMode
2022-02-06 09:43:52 +01:00
if u . cursorMode == CursorModeCaptured {
2021-04-15 19:26:10 +02:00
document . Call ( "exitPointerLock" )
2023-07-06 18:17:28 +02:00
u . lastCaptureExitTime = time . Now ( )
2021-04-15 19:26:10 +02:00
}
u . cursorMode = mode
2019-12-14 04:30:03 +01:00
switch mode {
2022-02-06 09:43:52 +01:00
case CursorModeVisible :
2021-04-15 19:26:10 +02:00
canvas . Get ( "style" ) . Set ( "cursor" , driverCursorShapeToCSSCursor ( u . cursorShape ) )
2022-02-06 09:43:52 +01:00
case CursorModeHidden :
2021-04-11 07:21:38 +02:00
canvas . Get ( "style" ) . Set ( "cursor" , stringNone )
2022-02-06 09:43:52 +01:00
case CursorModeCaptured :
2021-04-15 19:26:10 +02:00
canvas . Call ( "requestPointerLock" )
2021-04-11 07:21:38 +02:00
}
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) recoverCursorMode ( ) {
2023-10-14 17:06:07 +02:00
if u . cursorPrevMode == CursorModeCaptured {
2022-02-06 09:43:52 +01:00
panic ( "ui: cursorPrevMode must not be CursorModeCaptured at recoverCursorMode" )
2021-04-15 20:13:12 +02:00
}
2021-04-15 19:26:10 +02:00
u . SetCursorMode ( u . cursorPrevMode )
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) CursorShape ( ) CursorShape {
2021-04-11 07:21:38 +02:00
if ! canvas . Truthy ( ) {
2022-02-06 09:43:52 +01:00
return CursorShapeDefault
2021-04-11 07:21:38 +02:00
}
return u . cursorShape
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) SetCursorShape ( shape CursorShape ) {
2021-04-11 07:21:38 +02:00
if ! canvas . Truthy ( ) {
return
}
if u . cursorShape == shape {
return
}
2021-04-15 19:26:10 +02:00
2021-04-11 07:21:38 +02:00
u . cursorShape = shape
2022-02-06 09:43:52 +01:00
if u . cursorMode == CursorModeVisible {
2021-04-11 07:21:38 +02:00
canvas . Get ( "style" ) . Set ( "cursor" , driverCursorShapeToCSSCursor ( u . cursorShape ) )
2016-09-03 10:17:54 +02:00
}
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) DeviceScaleFactor ( ) float64 {
2023-09-23 20:03:07 +02:00
if u . deviceScaleFactor != 0 {
return u . deviceScaleFactor
}
ratio := window . Get ( "devicePixelRatio" ) . Float ( )
if ratio == 0 {
ratio = 1
}
u . deviceScaleFactor = ratio
return u . deviceScaleFactor
2016-05-19 17:07:04 +02:00
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) outsideSize ( ) ( float64 , float64 ) {
2022-08-07 15:17:47 +02:00
if document . Truthy ( ) {
2022-02-13 18:50:32 +01:00
body := document . Get ( "body" )
bw := body . Get ( "clientWidth" ) . Float ( )
bh := body . Get ( "clientHeight" ) . Float ( )
2022-02-13 18:48:28 +01:00
return bw , bh
2018-02-01 18:08:03 +01:00
}
2022-08-07 15:17:47 +02:00
// Node.js
return 640 , 480
2018-02-01 18:08:03 +01:00
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) suspended ( ) bool {
2020-03-20 16:34:12 +01:00
if u . runnableOnUnfocused {
2019-10-20 17:02:35 +02:00
return false
}
2020-03-20 16:08:40 +01:00
return ! u . isFocused ( )
2020-01-21 15:56:47 +01:00
}
2019-10-20 17:02:35 +02:00
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) isFocused ( ) bool {
2021-09-04 12:20:24 +02:00
if ! documentHasFocus . Invoke ( ) . Bool ( ) {
2020-01-21 15:56:47 +01:00
return false
2019-10-20 17:02:35 +02:00
}
2021-09-04 12:20:24 +02:00
if documentHidden . Invoke ( ) . Bool ( ) {
2020-01-21 15:56:47 +01:00
return false
2019-10-20 17:02:35 +02:00
}
2020-01-21 15:56:47 +01:00
return true
2018-05-26 11:04:20 +02:00
}
2023-07-06 18:17:28 +02:00
// canCaptureCursor reports whether a cursor can be captured or not now.
// Just after escaping from a capture, a browser might not be able to capture a cursor (#2693).
// If it is too early to capture a cursor, Ebitengine tries to delay it.
//
// See also https://w3c.github.io/pointerlock/#extensions-to-the-element-interface
//
// > Pointer lock is a transient activation-gated API, therefore a requestPointerLock() call
// > MUST fail if the relevant global object of this does not have transient activation.
// > This prevents locking upon initial navigation or re-acquiring lock without user's attention.
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) canCaptureCursor ( ) bool {
2023-07-06 18:17:28 +02:00
// 1.5 [sec] seems enough in the real world.
return time . Now ( ) . Sub ( u . lastCaptureExitTime ) >= 1500 * time . Millisecond
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) update ( ) error {
2023-07-06 18:17:28 +02:00
if u . captureCursorLater && u . canCaptureCursor ( ) {
u . setCursorMode ( CursorModeCaptured )
}
2018-05-26 11:04:20 +02:00
if u . suspended ( ) {
2023-10-06 06:49:42 +02:00
return hook . SuspendAudio ( )
2021-05-04 15:24:31 +02:00
}
2023-10-06 06:49:42 +02:00
if err := hook . ResumeAudio ( ) ; err != nil {
2021-05-04 15:24:31 +02:00
return err
2016-06-29 18:07:54 +02:00
}
2021-01-17 17:17:55 +01:00
return u . updateImpl ( false )
}
2018-05-26 11:04:20 +02:00
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) updateImpl ( force bool ) error {
2022-09-20 06:19:40 +02:00
// Guard updateImpl as this function cannot be invoked until this finishes (#2339).
u . m . Lock ( )
defer u . m . Unlock ( )
2022-01-03 07:17:26 +01:00
// context can be nil when an event is fired but the loop doesn't start yet (#1928).
if u . context == nil {
return nil
}
2022-06-24 13:20:49 +02:00
if err := gamepad . Update ( ) ; err != nil {
return err
}
2022-02-13 18:48:28 +01:00
2023-09-23 20:03:07 +02:00
if ! u . onceUpdateCalled {
2022-02-13 18:48:28 +01:00
u . updateScreenSize ( )
}
2023-09-23 20:03:07 +02:00
// TODO: If DeviceScaleFactor changes, call updateScreenSize.
// Now there is not a good way to detect the change.
// See also https://crbug.com/123694.
2022-02-13 18:48:28 +01:00
w , h := u . outsideSize ( )
2021-01-17 17:17:55 +01:00
if force {
2023-07-29 19:25:10 +02:00
if err := u . context . forceUpdateFrame ( u . graphicsDriver , w , h , u . DeviceScaleFactor ( ) , u , nil ) ; err != nil {
2021-01-17 17:17:55 +01:00
return err
}
} else {
2023-07-29 19:25:10 +02:00
if err := u . context . updateFrame ( u . graphicsDriver , w , h , u . DeviceScaleFactor ( ) , u , nil ) ; err != nil {
2021-01-17 17:17:55 +01:00
return err
}
2020-03-31 19:55:48 +02:00
}
2016-09-01 17:53:05 +02:00
return nil
2015-01-07 17:32:37 +01:00
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) needsUpdate ( ) bool {
2022-02-06 09:43:52 +01:00
if u . fpsMode != FPSModeVsyncOffMinimum {
2021-07-22 18:07:28 +02:00
return true
}
if ! u . onceUpdateCalled {
return true
}
if u . renderingScheduled {
return true
}
// TODO: Watch the gamepad state?
return false
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) loop ( game Game ) <- chan error {
2022-04-01 10:59:44 +02:00
u . context = newContext ( game )
2019-04-09 05:03:56 +02:00
2021-05-04 15:24:31 +02:00
errCh := make ( chan error , 1 )
2020-02-08 19:39:04 +01:00
reqStopAudioCh := make ( chan struct { } )
resStopAudioCh := make ( chan struct { } )
2019-04-30 19:15:28 +02:00
var cf js . Func
2020-05-18 20:28:14 +02:00
f := func ( ) {
2023-10-15 08:50:39 +02:00
if err := u . error ( ) ; err != nil {
2023-10-04 17:39:15 +02:00
errCh <- err
2022-09-09 15:47:54 +02:00
return
}
2021-07-22 18:07:28 +02:00
if u . needsUpdate ( ) {
2023-09-23 20:03:07 +02:00
defer func ( ) {
u . onceUpdateCalled = true
} ( )
2021-07-22 18:07:28 +02:00
u . renderingScheduled = false
if err := u . update ( ) ; err != nil {
close ( reqStopAudioCh )
<- resStopAudioCh
errCh <- err
return
}
2018-05-26 18:59:35 +02:00
}
2021-07-22 18:07:28 +02:00
switch u . fpsMode {
2022-02-06 09:43:52 +01:00
case FPSModeVsyncOn :
2018-07-14 13:50:09 +02:00
requestAnimationFrame . Invoke ( cf )
2022-02-06 09:43:52 +01:00
case FPSModeVsyncOffMaximum :
2018-07-14 16:47:31 +02:00
setTimeout . Invoke ( cf , 0 )
2022-02-06 09:43:52 +01:00
case FPSModeVsyncOffMinimum :
2021-07-22 18:07:28 +02:00
requestAnimationFrame . Invoke ( cf )
2018-07-14 13:50:09 +02:00
}
2016-02-27 19:49:57 +01:00
}
2020-05-18 21:08:30 +02:00
2019-04-30 19:15:28 +02:00
// TODO: Should cf be released after the game ends?
2022-11-03 07:33:09 +01:00
cf = js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
2020-05-18 21:08:30 +02:00
// f can be blocked but callbacks must not be blocked. Create a goroutine (#1161).
2020-05-18 20:28:14 +02:00
go f ( )
return nil
} )
2021-09-13 14:53:02 +02:00
// Call f asyncly since ch is used in f.
2020-05-18 20:28:14 +02:00
go f ( )
2015-01-02 07:20:05 +01:00
2019-10-20 17:02:35 +02:00
// Run another loop to watch suspended() as the above update function is never called when the tab is hidden.
// To check the document's visiblity, visibilitychange event should usually be used. However, this event is
// not reliable and sometimes it is not fired (#961). Then, watch the state regularly instead.
go func ( ) {
2020-02-08 19:39:04 +01:00
defer close ( resStopAudioCh )
const interval = 100 * time . Millisecond
t := time . NewTicker ( interval )
defer func ( ) {
t . Stop ( )
// This is a dirty hack. (*time.Ticker).Stop() just marks the timer 'deleted' [1] and
// something might run even after Stop. On Wasm, this causes an issue to execute Go program
// even after finishing (#1027). Sleep for the interval time duration to ensure that
// everything related to the timer is finished.
//
// [1] runtime.deltimer
time . Sleep ( interval )
} ( )
for {
select {
case <- t . C :
if u . suspended ( ) {
2023-10-06 06:49:42 +02:00
if err := hook . SuspendAudio ( ) ; err != nil {
2021-05-04 15:24:31 +02:00
errCh <- err
return
}
2020-02-08 19:39:04 +01:00
} else {
2023-10-06 06:49:42 +02:00
if err := hook . ResumeAudio ( ) ; err != nil {
2021-05-04 15:24:31 +02:00
errCh <- err
return
}
2020-02-08 19:39:04 +01:00
}
case <- reqStopAudioCh :
return
2019-10-20 17:02:35 +02:00
}
2018-05-26 11:04:20 +02:00
}
2019-10-20 17:02:35 +02:00
} ( )
2020-02-08 19:39:04 +01:00
return errCh
2019-10-13 19:41:04 +02:00
}
2023-10-14 17:06:07 +02:00
func ( u * UserInterface ) init ( ) error {
u . userInterfaceImpl = userInterfaceImpl {
2023-10-14 17:15:20 +02:00
runnableOnUnfocused : true ,
savedCursorX : math . NaN ( ) ,
savedCursorY : math . NaN ( ) ,
}
2020-11-22 10:22:52 +01:00
// docuemnt is undefined on node.js
if ! document . Truthy ( ) {
2023-10-14 17:06:07 +02:00
return nil
2020-11-22 10:22:52 +01:00
}
if ! document . Get ( "body" ) . Truthy ( ) {
2019-10-13 19:41:04 +02:00
ch := make ( chan struct { } )
2022-11-03 07:33:09 +01:00
window . Call ( "addEventListener" , "load" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
2019-10-13 19:41:04 +02:00
close ( ch )
return nil
} ) )
<- ch
}
2023-10-14 17:06:07 +02:00
u . setWindowEventHandlers ( window )
2015-01-07 03:22:48 +01:00
2017-07-01 21:08:25 +02:00
// Adjust the initial scale to 1.
// https://developer.mozilla.org/en/docs/Mozilla/Mobile/Viewport_meta_tag
2018-06-18 20:40:37 +02:00
meta := document . Call ( "createElement" , "meta" )
2017-07-01 21:08:25 +02:00
meta . Set ( "name" , "viewport" )
meta . Set ( "content" , "width=device-width, initial-scale=1" )
2018-06-18 20:40:37 +02:00
document . Get ( "head" ) . Call ( "appendChild" , meta )
2017-07-01 21:08:25 +02:00
2018-06-18 20:40:37 +02:00
canvas = document . Call ( "createElement" , "canvas" )
2015-01-02 08:48:07 +01:00
canvas . Set ( "width" , 16 )
canvas . Set ( "height" , 16 )
2019-12-09 18:37:10 +01:00
2018-06-18 20:40:37 +02:00
document . Get ( "body" ) . Call ( "appendChild" , canvas )
2015-01-05 14:22:47 +01:00
2018-06-18 20:40:37 +02:00
htmlStyle := document . Get ( "documentElement" ) . Get ( "style" )
2015-01-05 14:22:47 +01:00
htmlStyle . Set ( "height" , "100%" )
2015-01-05 16:44:39 +01:00
htmlStyle . Set ( "margin" , "0" )
htmlStyle . Set ( "padding" , "0" )
2015-01-05 14:22:47 +01:00
2018-06-18 20:40:37 +02:00
bodyStyle := document . Get ( "body" ) . Get ( "style" )
2015-01-05 14:22:47 +01:00
bodyStyle . Set ( "backgroundColor" , "#000" )
bodyStyle . Set ( "height" , "100%" )
2015-01-05 16:44:39 +01:00
bodyStyle . Set ( "margin" , "0" )
bodyStyle . Set ( "padding" , "0" )
2019-03-19 17:36:52 +01:00
2015-01-05 14:22:47 +01:00
canvasStyle := canvas . Get ( "style" )
2019-12-09 18:37:10 +01:00
canvasStyle . Set ( "width" , "100%" )
canvasStyle . Set ( "height" , "100%" )
canvasStyle . Set ( "margin" , "0" )
canvasStyle . Set ( "padding" , "0" )
2015-01-05 14:22:47 +01:00
2015-01-02 16:52:49 +01:00
// Make the canvas focusable.
canvas . Call ( "setAttribute" , "tabindex" , 1 )
canvas . Get ( "style" ) . Set ( "outline" , "none" )
2023-10-14 17:06:07 +02:00
u . setCanvasEventHandlers ( canvas )
2021-04-15 19:26:10 +02:00
// Pointer Lock
2022-11-03 07:33:09 +01:00
document . Call ( "addEventListener" , "pointerlockchange" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
2021-04-15 19:26:10 +02:00
if document . Get ( "pointerLockElement" ) . Truthy ( ) {
return nil
}
2021-04-15 20:14:46 +02:00
// Recover the state correctly when the pointer lock exits.
2021-04-15 19:26:10 +02:00
// A user can exit the pointer lock by pressing ESC. In this case, sync the cursor mode state.
2023-10-14 17:06:07 +02:00
if u . cursorMode == CursorModeCaptured {
u . recoverCursorMode ( )
2021-04-15 19:26:10 +02:00
}
2023-10-14 17:06:07 +02:00
u . recoverCursorPosition ( )
2021-04-15 19:26:10 +02:00
return nil
} ) )
2022-11-03 07:33:09 +01:00
document . Call ( "addEventListener" , "pointerlockerror" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
2021-04-28 16:28:55 +02:00
js . Global ( ) . Get ( "console" ) . Call ( "error" , "pointerlockerror event is fired. 'sandbox=\"allow-pointer-lock\"' might be required at an iframe. This function on browsers must be called as a result of a gestural interaction or orientation change." )
2021-04-20 05:22:31 +02:00
return nil
} ) )
2022-11-03 07:33:09 +01:00
document . Call ( "addEventListener" , "fullscreenerror" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
2022-04-09 09:55:09 +02:00
js . Global ( ) . Get ( "console" ) . Call ( "error" , "fullscreenerror event is fired. 'allow=\"fullscreen\"' or 'allowfullscreen' might be required at an iframe. This function on browsers must be called as a result of a gestural interaction or orientation change." )
2021-04-28 16:28:55 +02:00
return nil
} ) )
2022-11-03 07:33:09 +01:00
document . Call ( "addEventListener" , "webkitfullscreenerror" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
2022-04-09 09:55:09 +02:00
js . Global ( ) . Get ( "console" ) . Call ( "error" , "webkitfullscreenerror event is fired. 'allow=\"fullscreen\"' or 'allowfullscreen' might be required at an iframe. This function on browsers must be called as a result of a gestural interaction or orientation change." )
2021-04-27 16:58:32 +02:00
return nil
} ) )
2023-10-14 17:06:07 +02:00
return nil
2020-12-06 14:56:09 +01:00
}
2023-10-14 17:06:07 +02:00
func ( u * UserInterface ) setWindowEventHandlers ( v js . Value ) {
2022-11-03 07:33:09 +01:00
v . Call ( "addEventListener" , "resize" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
2023-10-14 17:06:07 +02:00
u . updateScreenSize ( )
2022-09-20 06:19:40 +02:00
// updateImpl can block. Use goroutine.
// See https://pkg.go.dev/syscall/js#FuncOf.
go func ( ) {
2023-10-14 17:06:07 +02:00
if err := u . updateImpl ( true ) ; err != nil {
2023-10-15 08:50:39 +02:00
u . setError ( err )
2022-09-20 06:19:40 +02:00
return
}
} ( )
2020-12-06 14:56:09 +01:00
return nil
} ) )
}
2023-10-14 17:06:07 +02:00
func ( u * UserInterface ) setCanvasEventHandlers ( v js . Value ) {
2015-01-12 06:36:13 +01:00
// Keyboard
2022-11-03 07:33:09 +01:00
v . Call ( "addEventListener" , "keydown" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
2019-10-15 16:57:14 +02:00
// Focus the canvas explicitly to activate tha game (#961).
2020-12-06 14:56:09 +01:00
v . Call ( "focus" )
2019-10-15 16:57:14 +02:00
2019-04-30 19:15:28 +02:00
e := args [ 0 ]
e . Call ( "preventDefault" )
2023-10-14 17:06:07 +02:00
if err := u . updateInputFromEvent ( e ) ; err != nil {
2023-10-15 08:50:39 +02:00
u . setError ( err )
2022-09-09 15:47:54 +02:00
return nil
}
2019-04-30 19:15:28 +02:00
return nil
2019-04-06 16:32:19 +02:00
} ) )
2022-11-03 07:33:09 +01:00
v . Call ( "addEventListener" , "keyup" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
2019-04-30 19:15:28 +02:00
e := args [ 0 ]
e . Call ( "preventDefault" )
2023-10-14 17:06:07 +02:00
if err := u . updateInputFromEvent ( e ) ; err != nil {
2023-10-15 08:50:39 +02:00
u . setError ( err )
2022-09-09 15:47:54 +02:00
return nil
}
2019-04-30 19:15:28 +02:00
return nil
2019-04-06 16:32:19 +02:00
} ) )
2015-01-12 06:36:13 +01:00
// Mouse
2022-11-03 07:33:09 +01:00
v . Call ( "addEventListener" , "mousedown" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
2019-10-15 16:57:14 +02:00
// Focus the canvas explicitly to activate tha game (#961).
2020-12-06 14:56:09 +01:00
v . Call ( "focus" )
2019-10-15 16:57:14 +02:00
2019-04-30 19:15:28 +02:00
e := args [ 0 ]
e . Call ( "preventDefault" )
2023-10-14 17:06:07 +02:00
if err := u . updateInputFromEvent ( e ) ; err != nil {
2023-10-15 08:50:39 +02:00
u . setError ( err )
2022-09-09 15:47:54 +02:00
return nil
}
2019-04-30 19:15:28 +02:00
return nil
2019-04-06 16:32:19 +02:00
} ) )
2022-11-03 07:33:09 +01:00
v . Call ( "addEventListener" , "mouseup" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
2019-04-30 19:15:28 +02:00
e := args [ 0 ]
e . Call ( "preventDefault" )
2023-10-14 17:06:07 +02:00
if err := u . updateInputFromEvent ( e ) ; err != nil {
2023-10-15 08:50:39 +02:00
u . setError ( err )
2022-09-09 15:47:54 +02:00
return nil
}
2019-04-30 19:15:28 +02:00
return nil
2019-04-06 16:32:19 +02:00
} ) )
2022-11-03 07:33:09 +01:00
v . Call ( "addEventListener" , "mousemove" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
2019-04-30 19:15:28 +02:00
e := args [ 0 ]
e . Call ( "preventDefault" )
2023-10-14 17:06:07 +02:00
if err := u . updateInputFromEvent ( e ) ; err != nil {
2023-10-15 08:50:39 +02:00
u . setError ( err )
2022-09-09 15:47:54 +02:00
return nil
}
2019-04-30 19:15:28 +02:00
return nil
2019-04-06 16:32:19 +02:00
} ) )
2022-11-03 07:33:09 +01:00
v . Call ( "addEventListener" , "wheel" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
2019-04-30 19:15:28 +02:00
e := args [ 0 ]
e . Call ( "preventDefault" )
2023-10-14 17:06:07 +02:00
if err := u . updateInputFromEvent ( e ) ; err != nil {
2023-10-15 08:50:39 +02:00
u . setError ( err )
2022-09-09 15:47:54 +02:00
return nil
}
2019-04-30 19:15:28 +02:00
return nil
2019-04-06 16:32:19 +02:00
} ) )
2015-01-12 06:36:13 +01:00
2016-05-26 18:31:30 +02:00
// Touch
2022-11-03 07:33:09 +01:00
v . Call ( "addEventListener" , "touchstart" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
2019-10-15 16:57:14 +02:00
// Focus the canvas explicitly to activate tha game (#961).
2020-12-06 14:56:09 +01:00
v . Call ( "focus" )
2019-10-15 16:57:14 +02:00
2019-04-30 19:15:28 +02:00
e := args [ 0 ]
e . Call ( "preventDefault" )
2023-10-14 17:06:07 +02:00
if err := u . updateInputFromEvent ( e ) ; err != nil {
2023-10-15 08:50:39 +02:00
u . setError ( err )
2022-09-09 15:47:54 +02:00
return nil
}
2019-04-30 19:15:28 +02:00
return nil
2019-04-06 16:32:19 +02:00
} ) )
2022-11-03 07:33:09 +01:00
v . Call ( "addEventListener" , "touchend" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
2019-04-30 19:15:28 +02:00
e := args [ 0 ]
e . Call ( "preventDefault" )
2023-10-14 17:06:07 +02:00
if err := u . updateInputFromEvent ( e ) ; err != nil {
2023-10-15 08:50:39 +02:00
u . setError ( err )
2022-09-09 15:47:54 +02:00
return nil
}
2019-04-30 19:15:28 +02:00
return nil
2019-04-06 16:32:19 +02:00
} ) )
2022-11-03 07:33:09 +01:00
v . Call ( "addEventListener" , "touchmove" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
2019-04-30 19:15:28 +02:00
e := args [ 0 ]
e . Call ( "preventDefault" )
2023-10-14 17:06:07 +02:00
if err := u . updateInputFromEvent ( e ) ; err != nil {
2023-10-15 08:50:39 +02:00
u . setError ( err )
2022-09-09 15:47:54 +02:00
return nil
}
2019-04-30 19:15:28 +02:00
return nil
2019-04-06 16:32:19 +02:00
} ) )
2015-01-20 18:58:29 +01:00
2021-04-15 19:26:10 +02:00
// Context menu
2022-11-03 07:33:09 +01:00
v . Call ( "addEventListener" , "contextmenu" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
2019-04-30 19:15:28 +02:00
e := args [ 0 ]
e . Call ( "preventDefault" )
return nil
2018-05-26 18:59:35 +02:00
} ) )
2017-01-20 18:47:31 +01:00
2019-01-29 17:44:57 +01:00
// Context
2022-11-03 07:33:09 +01:00
v . Call ( "addEventListener" , "webglcontextlost" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
2019-04-30 19:15:28 +02:00
e := args [ 0 ]
e . Call ( "preventDefault" )
2021-06-26 09:50:12 +02:00
window . Get ( "location" ) . Call ( "reload" )
2019-04-30 19:15:28 +02:00
return nil
2018-05-26 18:59:35 +02:00
} ) )
2023-01-21 15:34:20 +01:00
// Drop
v . Call ( "addEventListener" , "dragover" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
e := args [ 0 ]
e . Call ( "preventDefault" )
return nil
} ) )
v . Call ( "addEventListener" , "drop" , js . FuncOf ( func ( this js . Value , args [ ] js . Value ) any {
e := args [ 0 ]
e . Call ( "preventDefault" )
data := e . Get ( "dataTransfer" )
if ! data . Truthy ( ) {
return nil
}
2023-10-14 17:06:07 +02:00
go u . appendDroppedFiles ( data )
2023-01-21 15:34:20 +01:00
return nil
} ) )
2015-01-02 07:20:05 +01:00
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) appendDroppedFiles ( data js . Value ) {
2023-01-22 09:43:56 +01:00
u . dropFileM . Lock ( )
defer u . dropFileM . Unlock ( )
items := data . Get ( "items" )
2023-01-22 18:48:50 +01:00
if items . Length ( ) <= 0 {
return
2023-01-22 09:43:56 +01:00
}
2023-01-22 18:48:50 +01:00
fs := items . Index ( 0 ) . Call ( "webkitGetAsEntry" ) . Get ( "filesystem" ) . Get ( "root" )
u . inputState . DroppedFiles = file . NewFileEntryFS ( fs )
2023-01-22 09:43:56 +01:00
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) forceUpdateOnMinimumFPSMode ( ) {
2022-02-06 09:43:52 +01:00
if u . fpsMode != FPSModeVsyncOffMinimum {
2022-09-20 06:19:40 +02:00
return
2021-07-22 18:07:28 +02:00
}
2022-09-20 06:19:40 +02:00
// updateImpl can block. Use goroutine.
// See https://pkg.go.dev/syscall/js#FuncOf.
go func ( ) {
2023-10-04 17:39:15 +02:00
if err := u . updateImpl ( true ) ; err != nil {
2023-10-15 08:50:39 +02:00
u . setError ( err )
2022-09-20 06:19:40 +02:00
}
} ( )
2021-07-22 18:07:28 +02:00
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) Run ( game Game , options * RunOptions ) error {
2022-12-09 11:04:52 +01:00
if ! options . InitUnfocused && window . Truthy ( ) {
2020-10-10 08:52:29 +02:00
// Do not focus the canvas when the current document is in an iframe.
// Otherwise, the parent page tries to focus the iframe on every loading, which is annoying (#1373).
2021-06-07 14:43:25 +02:00
isInIframe := ! window . Get ( "location" ) . Equal ( window . Get ( "parent" ) . Get ( "location" ) )
2020-10-10 08:52:29 +02:00
if ! isInIframe {
canvas . Call ( "focus" )
}
2020-02-09 15:03:03 +01:00
}
2019-11-30 16:07:41 +01:00
u . running = true
2023-10-15 09:38:05 +02:00
g , lib , err := newGraphicsDriver ( & graphicsDriverCreatorImpl {
2022-11-13 07:18:44 +01:00
canvas : canvas ,
2022-12-09 06:07:04 +01:00
} , options . GraphicsLibrary )
2022-03-22 15:33:21 +01:00
if err != nil {
return err
}
u . graphicsDriver = g
2023-10-15 08:50:39 +02:00
u . setGraphicsLibrary ( lib )
2022-12-09 13:23:41 +01:00
if bodyStyle := document . Get ( "body" ) . Get ( "style" ) ; options . ScreenTransparent {
bodyStyle . Set ( "backgroundColor" , "transparent" )
} else {
bodyStyle . Set ( "backgroundColor" , "#000" )
}
2022-02-13 12:18:41 +01:00
return <- u . loop ( game )
2015-02-09 02:25:00 +01:00
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) updateScreenSize ( ) {
2022-08-07 15:17:47 +02:00
if document . Truthy ( ) {
2020-11-22 10:22:52 +01:00
body := document . Get ( "body" )
bw := int ( body . Get ( "clientWidth" ) . Float ( ) * u . DeviceScaleFactor ( ) )
bh := int ( body . Get ( "clientHeight" ) . Float ( ) * u . DeviceScaleFactor ( ) )
canvas . Set ( "width" , bw )
canvas . Set ( "height" , bh )
}
2015-01-02 07:20:05 +01:00
}
2019-04-07 11:28:50 +02:00
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) readInputState ( inputState * InputState ) {
2023-01-21 16:04:30 +01:00
u . inputState . copyAndReset ( inputState )
2022-08-13 06:33:52 +02:00
u . keyboardLayoutMap = js . Value { }
2019-04-07 11:28:50 +02:00
}
2019-12-24 16:05:56 +01:00
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) Window ( ) Window {
2022-05-31 18:26:28 +02:00
return & nullWindow { }
2019-12-24 16:05:56 +01:00
}
2022-09-25 17:34:11 +02:00
2023-08-30 14:02:04 +02:00
type Monitor struct { }
var theMonitor = & Monitor { }
func ( m * Monitor ) Bounds ( ) image . Rectangle {
screen := window . Get ( "screen" )
w := screen . Get ( "width" ) . Int ( )
h := screen . Get ( "height" ) . Int ( )
return image . Rect ( 0 , 0 , w , h )
}
func ( m * Monitor ) Name ( ) string {
return ""
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) AppendMonitors ( mons [ ] * Monitor ) [ ] * Monitor {
2023-08-30 14:02:04 +02:00
return append ( mons , theMonitor )
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) Monitor ( ) * Monitor {
2023-08-30 14:02:04 +02:00
return theMonitor
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) beginFrame ( ) {
2022-09-25 17:34:11 +02:00
}
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) endFrame ( ) {
2022-09-25 17:34:11 +02:00
}
2022-12-09 13:23:41 +01:00
2023-10-14 19:51:23 +02:00
func ( u * UserInterface ) updateIconIfNeeded ( ) error {
2023-04-19 17:56:08 +02:00
return nil
2022-12-29 15:48:53 +01:00
}
2022-12-09 13:23:41 +01:00
func IsScreenTransparentAvailable ( ) bool {
return true
}