vendor dependencies, make some changes to how input is done

This commit is contained in:
2026-06-15 18:54:00 +02:00
parent 535130933c
commit 4800eb28d9
759 changed files with 360941 additions and 30 deletions
+160
View File
@@ -0,0 +1,160 @@
// Copyright 2022 The Ebitengine Authors
//
// 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.
package ui
import (
"errors"
"fmt"
"runtime"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
const (
_CLSCTX_INPROC_SERVER = 0x1
_CLSCTX_LOCAL_SERVER = 0x4
_CLSCTX_REMOTE_SERVER = 0x10
_CLSCTX_SERVER = _CLSCTX_INPROC_SERVER | _CLSCTX_LOCAL_SERVER | _CLSCTX_REMOTE_SERVER
_MONITOR_DEFAULTTONEAREST = 2
_SM_CYCAPTION = 4
)
var (
_CLSID_TaskbarList = windows.GUID{
Data1: 0x56FDF344,
Data2: 0xFD6D,
Data3: 0x11D0,
Data4: [...]byte{0x95, 0x8A, 0x00, 0x60, 0x97, 0xC9, 0xA0, 0x90},
}
_IID_ITaskbarList = windows.GUID{
Data1: 0x56FDF342,
Data2: 0xFD6D,
Data3: 0x11D0,
Data4: [...]byte{0x95, 0x8A, 0x00, 0x60, 0x97, 0xC9, 0xA0, 0x90},
}
)
type _RECT struct {
left int32
top int32
right int32
bottom int32
}
type _MONITORINFO struct {
cbSize uint32
rcMonitor _RECT
rcWork _RECT
dwFlags uint32
}
type _POINT struct {
x int32
y int32
}
var (
ole32 = windows.NewLazySystemDLL("ole32.dll")
user32 = windows.NewLazySystemDLL("user32.dll")
procCoCreateInstance = ole32.NewProc("CoCreateInstance")
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
procMonitorFromWindow = user32.NewProc("MonitorFromWindow")
procGetMonitorInfoW = user32.NewProc("GetMonitorInfoW")
procGetCursorPos = user32.NewProc("GetCursorPos")
)
func _CoCreateInstance(rclsid *windows.GUID, pUnkOuter unsafe.Pointer, dwClsContext uint32, riid *windows.GUID) (unsafe.Pointer, error) {
var ptr unsafe.Pointer
r, _, _ := procCoCreateInstance.Call(uintptr(unsafe.Pointer(rclsid)), uintptr(pUnkOuter), uintptr(dwClsContext), uintptr(unsafe.Pointer(riid)), uintptr(unsafe.Pointer(&ptr)))
runtime.KeepAlive(rclsid)
runtime.KeepAlive(riid)
if uint32(r) != uint32(windows.S_OK) {
return nil, fmt.Errorf("ui: CoCreateInstance failed: error code: HRESULT(%d)", uint32(r))
}
return ptr, nil
}
func _GetSystemMetrics(nIndex int) (int32, error) {
r, _, _ := procGetSystemMetrics.Call(uintptr(nIndex))
if int32(r) == 0 {
// GetLastError doesn't provide an extended information.
// See https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetrics
return 0, fmt.Errorf("ui: GetSystemMetrics returned 0")
}
return int32(r), nil
}
func _MonitorFromWindow(hwnd windows.HWND, dwFlags uint32) uintptr {
r, _, _ := procMonitorFromWindow.Call(uintptr(hwnd), uintptr(dwFlags))
return r
}
func _GetMonitorInfoW(hMonitor uintptr) (_MONITORINFO, error) {
mi := _MONITORINFO{}
mi.cbSize = uint32(unsafe.Sizeof(mi))
r, _, e := procGetMonitorInfoW.Call(hMonitor, uintptr(unsafe.Pointer(&mi)))
if int32(r) == 0 {
if e != nil && !errors.Is(e, windows.ERROR_SUCCESS) {
return _MONITORINFO{}, fmt.Errorf("ui: GetMonitorInfoW failed: error code: %w", e)
}
return _MONITORINFO{}, fmt.Errorf("ui: GetMonitorInfoW failed: returned 0")
}
return mi, nil
}
func _GetCursorPos() (int32, int32, error) {
var pt _POINT
r, _, e := procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
if int32(r) == 0 {
if e != nil && !errors.Is(e, windows.ERROR_SUCCESS) {
return 0, 0, fmt.Errorf("ui: GetCursorPos failed: error code: %w", e)
}
return 0, 0, fmt.Errorf("ui: GetCursorPos failed: returned 0")
}
return pt.x, pt.y, nil
}
type _ITaskbarList struct {
vtbl *_ITaskbarList_Vtbl
}
type _ITaskbarList_Vtbl struct {
QueryInterface uintptr
AddRef uintptr
Release uintptr
HrInit uintptr
AddTab uintptr
DeleteTab uintptr
ActivateTab uintptr
SetActiveAlt uintptr
}
func (i *_ITaskbarList) DeleteTab(hwnd windows.HWND) error {
r, _, _ := syscall.Syscall(i.vtbl.DeleteTab, 2, uintptr(unsafe.Pointer(i)), uintptr(hwnd), 0)
if uint32(r) != uint32(windows.S_OK) {
return fmt.Errorf("ui: ITaskbarList::DeleteTab failed: HRESULT(%d)", uint32(r))
}
return nil
}
func (i *_ITaskbarList) Release() {
_, _, _ = syscall.Syscall(i.vtbl.Release, 1, uintptr(unsafe.Pointer(i)), 0, 0)
}
+331
View File
@@ -0,0 +1,331 @@
// Copyright 2022 The Ebiten Authors
//
// 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.
package ui
import (
"math"
"time"
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
"github.com/hajimehoshi/ebiten/v2/internal/clock"
"github.com/hajimehoshi/ebiten/v2/internal/debug"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/hook"
)
var (
NearestFilterShader = &Shader{shader: atlas.NearestFilterShader}
LinearFilterShader = &Shader{shader: atlas.LinearFilterShader}
)
type Game interface {
NewOffscreenImage(width, height int) *Image
NewScreenImage(width, height int) *Image
Layout(outsideWidth, outsideHeight float64) (screenWidth, screenHeight float64)
UpdateInputState(fn func(*InputState))
Update() error
DrawOffscreen() error
DrawFinalScreen(scale, offsetX, offsetY float64)
}
type context struct {
game Game
updateCalled bool
offscreen *Image
screen *Image
screenWidth float64
screenHeight float64
offscreenWidth float64
offscreenHeight float64
isOffscreenModified bool
lastDrawTime time.Time
skipCount int
funcsInFrameCh chan func()
}
func newContext(game Game) *context {
return &context{
game: game,
funcsInFrameCh: make(chan func()),
}
}
func (c *context) updateFrame(graphicsDriver graphicsdriver.Graphics, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *UserInterface) error {
// TODO: If updateCount is 0 and vsync is disabled, swapping buffers can be skipped.
return c.updateFrameImpl(graphicsDriver, clock.UpdateFrame(), outsideWidth, outsideHeight, deviceScaleFactor, ui, false)
}
func (c *context) forceUpdateFrame(graphicsDriver graphicsdriver.Graphics, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *UserInterface) error {
n := 1
if ui.GraphicsLibrary() == GraphicsLibraryDirectX {
// On DirectX, both framebuffers in the swap chain should be updated.
// Or, the rendering result becomes unexpected when the window is resized.
n = 2
}
for i := 0; i < n; i++ {
if err := c.updateFrameImpl(graphicsDriver, 1, outsideWidth, outsideHeight, deviceScaleFactor, ui, true); err != nil {
return err
}
}
return nil
}
func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, updateCount int, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *UserInterface, forceDraw bool) (err error) {
// The given outside size can be 0 e.g. just after restoring from the fullscreen mode on Windows (#1589)
// Just ignore such cases. Otherwise, creating a zero-sized framebuffer causes a panic.
if outsideWidth == 0 || outsideHeight == 0 {
return nil
}
debug.Logf("----\n")
if err := atlas.BeginFrame(graphicsDriver); err != nil {
return err
}
defer func() {
if err1 := atlas.EndFrame(); err1 != nil && err == nil {
err = err1
return
}
if err1 := atlas.SwapBuffers(graphicsDriver); err1 != nil && err == nil {
err = err1
return
}
}()
// Flush deferred functions, like reading pixels from GPU.
if err := c.processFuncsInFrame(ui); err != nil {
return err
}
// ForceUpdate can be invoked even if the context is not initialized yet (#1591).
if w, h := c.layoutGame(outsideWidth, outsideHeight, deviceScaleFactor); w == 0 || h == 0 {
return nil
}
// Update the input state after the layout is updated as a cursor position is affected by the layout.
if err := ui.updateInputState(); err != nil {
return err
}
// Ensure that Update is called once before Draw so that Update can be used for initialization.
if !c.updateCalled && updateCount == 0 {
updateCount = 1
c.updateCalled = true
}
debug.Logf("Update count per frame: %d\n", updateCount)
// Update the game.
for i := 0; i < updateCount; i++ {
// Read the input state and use it for one tick to give a consistent result for one tick (#2496, #2501).
c.game.UpdateInputState(func(inputState *InputState) {
ui.readInputState(inputState)
})
if err := hook.RunBeforeUpdateHooks(); err != nil {
return err
}
if err := c.game.Update(); err != nil {
return err
}
// Catch the error that happened at (*Image).At.
if err := ui.error(); err != nil {
return err
}
}
// Update window icons during a frame, since an icon might be *ebiten.Image and
// getting pixels from it needs to be in a frame (#1468).
if err := ui.updateIconIfNeeded(); err != nil {
return err
}
// Draw the game.
if err := c.drawGame(graphicsDriver, ui, forceDraw); err != nil {
return err
}
return nil
}
func (c *context) newOffscreenImage(w, h int) *Image {
img := c.game.NewOffscreenImage(w, h)
img.modifyCallback = func() {
c.isOffscreenModified = true
}
return img
}
func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics, ui *UserInterface, forceDraw bool) error {
if (c.offscreen.imageType == atlas.ImageTypeVolatile) != ui.IsScreenClearedEveryFrame() {
w, h := c.offscreen.width, c.offscreen.height
c.offscreen.Deallocate()
c.offscreen = c.newOffscreenImage(w, h)
}
// isOffscreenModified is updated when an offscreen's modifyCallback.
c.isOffscreenModified = false
// Even though updateCount == 0, the offscreen is cleared and Draw is called.
// Draw should not update the game state and then the screen should not be updated without Update, but
// users might want to process something at Draw with the time intervals of FPS.
if ui.IsScreenClearedEveryFrame() {
c.offscreen.clear()
}
if err := c.game.DrawOffscreen(); err != nil {
return err
}
const maxSkipCount = 3
if !forceDraw && !c.isOffscreenModified {
if c.skipCount < maxSkipCount {
c.skipCount++
}
} else {
c.skipCount = 0
}
now := time.Now()
defer func() {
c.lastDrawTime = now
}()
if c.skipCount < maxSkipCount {
if graphicsDriver.NeedsClearingScreen() {
// This clear is needed for fullscreen mode or some mobile platforms (#622).
c.screen.clear()
}
c.game.DrawFinalScreen(c.screenScaleAndOffsets())
// The final screen is never used as the rendering source.
// Flush its buffer here just in case.
c.screen.flushBufferIfNeeded()
} else if delta := time.Second/60 - now.Sub(c.lastDrawTime); delta > 0 {
// When swapping buffers is skipped and Draw is called too early, sleep for a while to suppress CPU usages (#2890).
time.Sleep(delta)
}
return nil
}
func (c *context) layoutGame(outsideWidth, outsideHeight float64, deviceScaleFactor float64) (int, int) {
owf, ohf := c.game.Layout(outsideWidth, outsideHeight)
if owf <= 0 || ohf <= 0 {
panic("ui: Layout must return positive numbers")
}
c.screenWidth = outsideWidth * deviceScaleFactor
c.screenHeight = outsideHeight * deviceScaleFactor
c.offscreenWidth = owf
c.offscreenHeight = ohf
sw := int(math.Ceil(c.screenWidth))
sh := int(math.Ceil(c.screenHeight))
ow := int(math.Ceil(c.offscreenWidth))
oh := int(math.Ceil(c.offscreenHeight))
if c.screen != nil && (c.screen.width != sw || c.screen.height != sh) {
c.screen.Deallocate()
c.screen = nil
}
if c.screen == nil {
c.screen = c.game.NewScreenImage(sw, sh)
}
if c.offscreen != nil && (c.offscreen.width != ow || c.offscreen.height != oh) {
c.offscreen.Deallocate()
c.offscreen = nil
}
if c.offscreen == nil {
c.offscreen = c.newOffscreenImage(ow, oh)
}
return ow, oh
}
func (c *context) clientPositionToLogicalPosition(x, y float64, deviceScaleFactor float64) (float64, float64) {
s, ox, oy := c.screenScaleAndOffsets()
// The scale 0 indicates that the screen is not initialized yet.
// As any cursor values don't make sense, just return NaN.
if s == 0 {
return math.NaN(), math.NaN()
}
return (x*deviceScaleFactor - ox) / s, (y*deviceScaleFactor - oy) / s
}
func (c *context) logicalPositionToClientPosition(x, y float64, deviceScaleFactor float64) (float64, float64) {
s, ox, oy := c.screenScaleAndOffsets()
return (x*s + ox) / deviceScaleFactor, (y*s + oy) / deviceScaleFactor
}
func (c *context) screenScaleAndOffsets() (scale, offsetX, offsetY float64) {
scaleX := c.screenWidth / c.offscreenWidth
scaleY := c.screenHeight / c.offscreenHeight
scale = math.Min(scaleX, scaleY)
width := c.offscreenWidth * scale
height := c.offscreenHeight * scale
offsetX = (c.screenWidth - width) / 2
offsetY = (c.screenHeight - height) / 2
return
}
func (u *UserInterface) LogicalPositionToClientPositionInNativePixels(x, y float64) (float64, float64) {
s := u.Monitor().DeviceScaleFactor()
x, y = u.context.logicalPositionToClientPosition(x, y, s)
x = dipToNativePixels(x, s)
y = dipToNativePixels(y, s)
return x, y
}
func (c *context) runInFrame(f func()) {
ch := make(chan struct{})
c.funcsInFrameCh <- func() {
defer close(ch)
f()
}
<-ch
return
}
func (c *context) processFuncsInFrame(ui *UserInterface) error {
var processed bool
for {
select {
case f := <-c.funcsInFrameCh:
f()
processed = true
default:
if processed {
// Catch the error that happened at (*Image).At.
if err := ui.error(); err != nil {
return err
}
}
return nil
}
}
}
+129
View File
@@ -0,0 +1,129 @@
// Copyright 2022 The Ebiten Authors
//
// 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.
package ui
import (
"fmt"
"os"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
)
type graphicsDriverCreator interface {
newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error)
newOpenGL() (graphicsdriver.Graphics, error)
newDirectX() (graphicsdriver.Graphics, error)
newMetal() (graphicsdriver.Graphics, error)
newPlayStation5() (graphicsdriver.Graphics, error)
}
func newGraphicsDriver(creator graphicsDriverCreator, graphicsLibrary GraphicsLibrary) (graphicsdriver.Graphics, GraphicsLibrary, error) {
if graphicsLibrary == GraphicsLibraryAuto {
envName := "EBITENGINE_GRAPHICS_LIBRARY"
env := os.Getenv(envName)
if env == "" {
// For backward compatibility, read the EBITEN_ version.
envName = "EBITEN_GRAPHICS_LIBRARY"
env = os.Getenv(envName)
}
switch env {
case "", "auto":
// Keep the automatic choosing.
case "opengl":
graphicsLibrary = GraphicsLibraryOpenGL
case "directx":
graphicsLibrary = GraphicsLibraryDirectX
case "metal":
graphicsLibrary = GraphicsLibraryMetal
case "playstation5":
graphicsLibrary = GraphicsLibraryPlayStation5
default:
return nil, 0, fmt.Errorf("ui: an unsupported graphics library is specified by the environment variable: %s", env)
}
}
switch graphicsLibrary {
case GraphicsLibraryAuto:
g, lib, err := creator.newAuto()
if err != nil {
return nil, 0, err
}
if g == nil {
return nil, 0, fmt.Errorf("ui: no graphics library is available")
}
return g, lib, nil
case GraphicsLibraryOpenGL:
g, err := creator.newOpenGL()
if err != nil {
return nil, 0, err
}
return g, GraphicsLibraryOpenGL, nil
case GraphicsLibraryDirectX:
g, err := creator.newDirectX()
if err != nil {
return nil, 0, err
}
return g, GraphicsLibraryDirectX, nil
case GraphicsLibraryMetal:
g, err := creator.newMetal()
if err != nil {
return nil, 0, err
}
return g, GraphicsLibraryMetal, nil
case GraphicsLibraryPlayStation5:
g, err := creator.newPlayStation5()
if err != nil {
return nil, 0, err
}
return g, GraphicsLibraryPlayStation5, nil
default:
return nil, 0, fmt.Errorf("ui: an unsupported graphics library is specified: %d", graphicsLibrary)
}
}
func (u *UserInterface) GraphicsDriverForTesting() graphicsdriver.Graphics {
return u.graphicsDriver
}
type GraphicsLibrary int
const (
GraphicsLibraryAuto GraphicsLibrary = iota
GraphicsLibraryUnknown
GraphicsLibraryOpenGL
GraphicsLibraryDirectX
GraphicsLibraryMetal
GraphicsLibraryPlayStation5
)
func (g GraphicsLibrary) String() string {
switch g {
case GraphicsLibraryAuto:
return "Auto"
case GraphicsLibraryUnknown:
return "Unknown"
case GraphicsLibraryOpenGL:
return "OpenGL"
case GraphicsLibraryDirectX:
return "DirectX"
case GraphicsLibraryMetal:
return "Metal"
case GraphicsLibraryPlayStation5:
return "PlayStation 5"
default:
return fmt.Sprintf("GraphicsLibrary(%d)", g)
}
}
+361
View File
@@ -0,0 +1,361 @@
// Copyright 2022 The Ebiten Authors
//
// 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.
package ui
import (
"fmt"
"image"
"math"
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
"github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/mipmap"
)
// panicOnErrorOnReadingPixels indicates whether reading pixels panics on an error or not.
// This value is set only on testing.
var panicOnErrorOnReadingPixels bool
func SetPanicOnErrorOnReadingPixelsForTesting(value bool) {
panicOnErrorOnReadingPixels = value
}
const bigOffscreenScale = 2
type Image struct {
ui *UserInterface
mipmap *mipmap.Mipmap
width int
height int
imageType atlas.ImageType
// lastBlend is the lastly-used blend for mipmap.Image.
lastBlend graphicsdriver.Blend
// bigOffscreenBuffer is a double-sized offscreen for anti-alias rendering.
bigOffscreenBuffer *bigOffscreenImage
// modifyCallback is a callback called when DrawTriangles or WritePixels is called.
// modifyCallback is useful to detect whether the image is manipulated or not after a certain time.
modifyCallback func()
tmpVerticesForFill []float32
}
func (u *UserInterface) NewImage(width, height int, imageType atlas.ImageType) *Image {
return &Image{
ui: u,
mipmap: mipmap.New(width, height, imageType),
width: width,
height: height,
imageType: imageType,
lastBlend: graphicsdriver.BlendSourceOver,
}
}
func (i *Image) Deallocate() {
if i.mipmap == nil {
return
}
if i.bigOffscreenBuffer != nil {
i.bigOffscreenBuffer.deallocate()
}
i.mipmap.Deallocate()
}
func (i *Image) DrawTriangles(srcs [graphics.ShaderImageCount]*Image, vertices []float32, indices []uint32, blend graphicsdriver.Blend, dstRegion image.Rectangle, srcRegions [graphics.ShaderImageCount]image.Rectangle, shader *Shader, uniforms []uint32, fillRule graphicsdriver.FillRule, canSkipMipmap bool, antialias bool) {
if i.modifyCallback != nil {
i.modifyCallback()
}
i.lastBlend = blend
if antialias {
if i.bigOffscreenBuffer == nil {
var imageType atlas.ImageType
switch i.imageType {
case atlas.ImageTypeRegular, atlas.ImageTypeUnmanaged:
imageType = atlas.ImageTypeUnmanaged
case atlas.ImageTypeScreen, atlas.ImageTypeVolatile:
imageType = atlas.ImageTypeVolatile
default:
panic(fmt.Sprintf("ui: unexpected image type: %d", imageType))
}
i.bigOffscreenBuffer = i.ui.newBigOffscreenImage(i, imageType)
}
i.bigOffscreenBuffer.drawTriangles(srcs, vertices, indices, blend, dstRegion, srcRegions, shader, uniforms, fillRule, canSkipMipmap, false)
return
}
i.flushBufferIfNeeded()
var srcMipmaps [graphics.ShaderImageCount]*mipmap.Mipmap
for i, src := range srcs {
if src == nil {
continue
}
src.flushBufferIfNeeded()
srcMipmaps[i] = src.mipmap
}
i.mipmap.DrawTriangles(srcMipmaps, vertices, indices, blend, dstRegion, srcRegions, shader.shader, uniforms, fillRule, canSkipMipmap)
}
func (i *Image) WritePixels(pix []byte, region image.Rectangle) {
if i.modifyCallback != nil {
i.modifyCallback()
}
i.flushBufferIfNeeded()
i.mipmap.WritePixels(pix, region)
}
func (i *Image) ReadPixels(pixels []byte, region image.Rectangle) {
// Check the error existence and avoid unnecessary calls.
if i.ui.error() != nil {
return
}
i.flushBigOffscreenBufferIfNeeded()
if err := i.ui.readPixels(i.mipmap, pixels, region); err != nil {
if panicOnErrorOnReadingPixels {
panic(err)
}
i.ui.setError(err)
}
}
func (i *Image) DumpScreenshot(name string, blackbg bool) (string, error) {
i.flushBufferIfNeeded()
return i.ui.dumpScreenshot(i.mipmap, name, blackbg)
}
func (i *Image) flushBufferIfNeeded() {
i.flushBigOffscreenBufferIfNeeded()
}
func (i *Image) flushBigOffscreenBufferIfNeeded() {
if i.bigOffscreenBuffer != nil {
i.bigOffscreenBuffer.flush()
}
}
func (u *UserInterface) DumpImages(dir string) (string, error) {
return u.dumpImages(dir)
}
func (i *Image) clear() {
i.Fill(0, 0, 0, 0, image.Rect(0, 0, i.width, i.height))
}
func (i *Image) Fill(r, g, b, a float32, region image.Rectangle) {
if len(i.tmpVerticesForFill) < 4*graphics.VertexFloatCount {
i.tmpVerticesForFill = make([]float32, 4*graphics.VertexFloatCount)
}
// i.tmpVerticesForFill can be reused as this is sent to DrawTriangles immediately.
graphics.QuadVertices(
i.tmpVerticesForFill,
1, 1, float32(i.ui.whiteImage.width-1), float32(i.ui.whiteImage.height-1),
float32(i.width), 0, 0, float32(i.height), 0, 0,
r, g, b, a)
is := graphics.QuadIndices()
srcs := [graphics.ShaderImageCount]*Image{i.ui.whiteImage}
blend := graphicsdriver.BlendCopy
// If possible, use BlendSourceOver to encourage batching (#2817).
if a == 1 && i.lastBlend == graphicsdriver.BlendSourceOver {
blend = graphicsdriver.BlendSourceOver
}
// i.lastBlend is updated in DrawTriangles.
i.DrawTriangles(srcs, i.tmpVerticesForFill, is, blend, region, [graphics.ShaderImageCount]image.Rectangle{}, NearestFilterShader, nil, graphicsdriver.FillAll, true, false)
}
type bigOffscreenImage struct {
ui *UserInterface
orig *Image
imageType atlas.ImageType
image *Image
region image.Rectangle
blend graphicsdriver.Blend
dirty bool
tmpVerticesForFlushing []float32
tmpVerticesForCopying []float32
}
func (u *UserInterface) newBigOffscreenImage(orig *Image, imageType atlas.ImageType) *bigOffscreenImage {
return &bigOffscreenImage{
ui: u,
orig: orig,
imageType: imageType,
}
}
func (i *bigOffscreenImage) deallocate() {
if i.image != nil {
i.image.Deallocate()
}
i.dirty = false
}
func (i *bigOffscreenImage) drawTriangles(srcs [graphics.ShaderImageCount]*Image, vertices []float32, indices []uint32, blend graphicsdriver.Blend, dstRegion image.Rectangle, srcRegions [graphics.ShaderImageCount]image.Rectangle, shader *Shader, uniforms []uint32, fillRule graphicsdriver.FillRule, canSkipMipmap bool, antialias bool) {
if i.blend != blend {
i.flush()
}
i.blend = blend
// If the new region doesn't match with the current region, remove the buffer image and recreate it later.
if r := i.requiredRegion(vertices); i.region != r {
i.flush()
i.image = nil
i.region = r
}
if i.region.Empty() {
return
}
if i.image == nil {
i.image = i.ui.NewImage(i.region.Dx()*bigOffscreenScale, i.region.Dy()*bigOffscreenScale, i.imageType)
}
// Copy the current rendering result to get the correct blending result.
if blend != graphicsdriver.BlendSourceOver && !i.dirty {
srcs := [graphics.ShaderImageCount]*Image{i.orig}
if len(i.tmpVerticesForCopying) < 4*graphics.VertexFloatCount {
i.tmpVerticesForCopying = make([]float32, 4*graphics.VertexFloatCount)
}
// i.tmpVerticesForCopying can be reused as this is sent to DrawTriangles immediately.
graphics.QuadVertices(
i.tmpVerticesForCopying,
float32(i.region.Min.X), float32(i.region.Min.Y), float32(i.region.Max.X), float32(i.region.Max.Y),
bigOffscreenScale, 0, 0, bigOffscreenScale, 0, 0,
1, 1, 1, 1)
is := graphics.QuadIndices()
dstRegion := image.Rect(0, 0, i.region.Dx()*bigOffscreenScale, i.region.Dy()*bigOffscreenScale)
i.image.DrawTriangles(srcs, i.tmpVerticesForCopying, is, graphicsdriver.BlendCopy, dstRegion, [graphics.ShaderImageCount]image.Rectangle{}, NearestFilterShader, nil, graphicsdriver.FillAll, true, false)
}
for idx := 0; idx < len(vertices); idx += graphics.VertexFloatCount {
vertices[idx] = (vertices[idx] - float32(i.region.Min.X)) * bigOffscreenScale
vertices[idx+1] = (vertices[idx+1] - float32(i.region.Min.Y)) * bigOffscreenScale
}
// Translate to i.region coordinate space, and clamp against region size.
dstRegion = dstRegion.Sub(i.region.Min)
dstRegion = dstRegion.Intersect(image.Rect(0, 0, i.region.Dx(), i.region.Dy()))
dstRegion.Min.X *= bigOffscreenScale
dstRegion.Min.Y *= bigOffscreenScale
dstRegion.Max.X *= bigOffscreenScale
dstRegion.Max.Y *= bigOffscreenScale
i.image.DrawTriangles(srcs, vertices, indices, blend, dstRegion, srcRegions, shader, uniforms, fillRule, canSkipMipmap, false)
i.dirty = true
}
func (i *bigOffscreenImage) flush() {
if i.image == nil {
return
}
if !i.dirty {
return
}
// Mark the offscreen clean earlier to avoid recursive calls.
i.dirty = false
srcs := [graphics.ShaderImageCount]*Image{i.image}
if len(i.tmpVerticesForFlushing) < 4*graphics.VertexFloatCount {
i.tmpVerticesForFlushing = make([]float32, 4*graphics.VertexFloatCount)
}
// i.tmpVerticesForFlushing can be reused as this is sent to DrawTriangles in this function.
graphics.QuadVertices(
i.tmpVerticesForFlushing,
0, 0, float32(i.region.Dx()*bigOffscreenScale), float32(i.region.Dy()*bigOffscreenScale),
1.0/bigOffscreenScale, 0, 0, 1.0/bigOffscreenScale, float32(i.region.Min.X), float32(i.region.Min.Y),
1, 1, 1, 1)
is := graphics.QuadIndices()
dstRegion := i.region
blend := graphicsdriver.BlendSourceOver
if i.blend != graphicsdriver.BlendSourceOver {
blend = graphicsdriver.BlendCopy
}
i.orig.DrawTriangles(srcs, i.tmpVerticesForFlushing, is, blend, dstRegion, [graphics.ShaderImageCount]image.Rectangle{}, LinearFilterShader, nil, graphicsdriver.FillAll, true, false)
i.image.clear()
i.dirty = false
}
func (i *bigOffscreenImage) requiredRegion(vertices []float32) image.Rectangle {
minX := float32(i.orig.width)
minY := float32(i.orig.height)
maxX := float32(0)
maxY := float32(0)
for i := 0; i < len(vertices); i += graphics.VertexFloatCount {
dstX := vertices[i]
dstY := vertices[i+1]
if minX > floor(dstX)-1 {
minX = floor(dstX) - 1
}
if minY > floor(dstY)-1 {
minY = floor(dstY) - 1
}
if maxX < ceil(dstX)+1 {
maxX = ceil(dstX) + 1
}
if maxY < ceil(dstY)+1 {
maxY = ceil(dstY) + 1
}
}
// Adjust the granularity of the rectangle.
r := image.Rect(
roundDown16(int(minX)),
roundDown16(int(minY)),
roundUp16(int(maxX)),
roundUp16(int(maxY)))
r = r.Intersect(image.Rect(0, 0, i.orig.width, i.orig.height))
// TODO: Is this check required?
if r.Dx() < 0 || r.Dy() < 0 {
return i.region
}
return r.Union(i.region)
}
func floor(x float32) float32 {
return float32(math.Floor(float64(x)))
}
func ceil(x float32) float32 {
return float32(math.Ceil(float64(x)))
}
func roundDown16(x int) int {
return x & ^(0xf)
}
func roundUp16(x int) int {
return ((x - 1) & ^(0xf)) + 0x10
}
@@ -0,0 +1,25 @@
// Copyright 2023 The Ebitengine Authors
//
// 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.
//go:build nintendosdk
// The actual implementation will be provided by -overlay.
#include "init_nintendosdk.h"
extern "C" NativeWindowType ebitengine_Initialize() { return 0; }
extern "C" void ebitengine_InitializeProfiler() {}
extern "C" void ebitengine_RecordProfilerHeartbeat() {}
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2023 The Ebitengine Authors
//
// 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.
//go:build nintendosdk
#include <EGL/egl.h>
#ifdef __cplusplus
extern "C" {
#endif
NativeWindowType ebitengine_Initialize();
void ebitengine_InitializeProfiler();
void ebitengine_RecordProfilerHeartbeat();
#ifdef __cplusplus
} // extern "C"
#endif
+81
View File
@@ -0,0 +1,81 @@
// Copyright 2022 The Ebitengine Authors
//
// 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.
package ui
import (
"io/fs"
"unicode"
)
type MouseButton int
const (
MouseButton0 MouseButton = iota // The 'left' button
MouseButton1 // The 'right' button
MouseButton2 // The 'middle' button
MouseButton3 // The additional button (usually browser-back)
MouseButton4 // The additional button (usually browser-forward)
MouseButtonMax = MouseButton4
)
type TouchID int
type Touch struct {
ID TouchID
X int
Y int
}
type InputState struct {
KeyPressed [KeyMax + 1]bool
MouseButtonPressed [MouseButtonMax + 1]bool
CursorX float64
CursorY float64
WheelX float64
WheelY float64
Touches []Touch
Runes []rune
WindowBeingClosed bool
DroppedFiles fs.FS
}
func (i *InputState) copyAndReset(dst *InputState) {
dst.KeyPressed = i.KeyPressed
dst.MouseButtonPressed = i.MouseButtonPressed
dst.CursorX = i.CursorX
dst.CursorY = i.CursorY
dst.WheelX = i.WheelX
dst.WheelY = i.WheelY
dst.Touches = append(dst.Touches[:0], i.Touches...)
dst.Runes = append(dst.Runes[:0], i.Runes...)
dst.WindowBeingClosed = i.WindowBeingClosed
dst.DroppedFiles = i.DroppedFiles
// Reset the members that are updated by deltas, rather than absolute values.
i.WheelX = 0
i.WheelY = 0
i.Runes = i.Runes[:0]
// Reset the members that are never reset until they are explicitly done.
i.WindowBeingClosed = false
i.DroppedFiles = nil
}
func (i *InputState) appendRune(r rune) {
if !unicode.IsPrint(r) {
return
}
i.Runes = append(i.Runes, r)
}
+156
View File
@@ -0,0 +1,156 @@
// 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.
//go:build !android && !ios && !js && !nintendosdk && !playstation5
package ui
import (
"math"
"github.com/hajimehoshi/ebiten/v2/internal/gamepad"
"github.com/hajimehoshi/ebiten/v2/internal/glfw"
)
var glfwMouseButtonToMouseButton = map[glfw.MouseButton]MouseButton{
glfw.MouseButtonLeft: MouseButton0,
glfw.MouseButtonMiddle: MouseButton1,
glfw.MouseButtonRight: MouseButton2,
glfw.MouseButton4: MouseButton3,
glfw.MouseButton5: MouseButton4,
}
func (u *UserInterface) registerInputCallbacks() error {
if _, err := u.window.SetCharModsCallback(func(w *glfw.Window, char rune, mods glfw.ModifierKey) {
// As this function is called from GLFW callbacks, the current thread is main.
u.m.Lock()
defer u.m.Unlock()
u.inputState.appendRune(char)
}); err != nil {
return err
}
if _, err := u.window.SetScrollCallback(func(w *glfw.Window, xoff float64, yoff float64) {
// As this function is called from GLFW callbacks, the current thread is main.
u.m.Lock()
defer u.m.Unlock()
u.inputState.WheelX += xoff
u.inputState.WheelY += yoff
}); err != nil {
return err
}
return nil
}
func (u *UserInterface) updateInputState() error {
var err error
u.mainThread.Call(func() {
err = u.updateInputStateImpl()
})
return err
}
// updateInputStateImpl must be called from the main thread.
func (u *UserInterface) updateInputStateImpl() error {
u.m.Lock()
defer u.m.Unlock()
for uk, gk := range uiKeyToGLFWKey {
s, err := u.window.GetKey(gk)
if err != nil {
return err
}
u.inputState.KeyPressed[uk] = s == glfw.Press
}
for gb, ub := range glfwMouseButtonToMouseButton {
s, err := u.window.GetMouseButton(gb)
if err != nil {
return err
}
u.inputState.MouseButtonPressed[ub] = s == glfw.Press
}
m, err := u.currentMonitor()
if err != nil {
return err
}
s := m.DeviceScaleFactor()
cx, cy := u.savedCursorX, u.savedCursorY
defer func() {
u.savedCursorX = math.NaN()
u.savedCursorY = math.NaN()
}()
if !math.IsNaN(cx) && !math.IsNaN(cy) {
cx2, cy2 := u.context.logicalPositionToClientPosition(cx, cy, s)
cx2 = dipToGLFWPixel(cx2, s)
cy2 = dipToGLFWPixel(cy2, s)
if err := u.window.SetCursorPos(cx2, cy2); err != nil {
return err
}
} else {
cx2, cy2, err := u.window.GetCursorPos()
if err != nil {
return err
}
cx2 = dipFromGLFWPixel(cx2, s)
cy2 = dipFromGLFWPixel(cy2, s)
cx, cy = u.context.clientPositionToLogicalPosition(cx2, cy2, s)
}
// AdjustPosition can return NaN at the initialization.
if !math.IsNaN(cx) && !math.IsNaN(cy) {
u.inputState.CursorX, u.inputState.CursorY = cx, cy
}
if err := gamepad.Update(); err != nil {
return err
}
return nil
}
func (u *UserInterface) KeyName(key Key) string {
if !u.isRunning() {
return ""
}
gk, ok := uiKeyToGLFWKey[key]
if !ok {
return ""
}
var name string
u.mainThread.Call(func() {
if u.isTerminated() {
return
}
n, err := glfw.GetKeyName(gk, 0)
if err != nil {
u.setError(err)
return
}
name = n
})
return name
}
func (u *UserInterface) saveCursorPosition() {
u.m.Lock()
defer u.m.Unlock()
u.savedCursorX = u.inputState.CursorX
u.savedCursorY = u.inputState.CursorY
}
+428
View File
@@ -0,0 +1,428 @@
// 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.
package ui
import (
"math"
"syscall/js"
"unicode"
)
var (
stringAlt = js.ValueOf("Alt")
stringControl = js.ValueOf("Control")
stringMeta = js.ValueOf("Meta")
stringShift = js.ValueOf("Shift")
stringKeydown = js.ValueOf("keydown")
stringKeyup = js.ValueOf("keyup")
stringMousedown = js.ValueOf("mousedown")
stringMouseup = js.ValueOf("mouseup")
stringMousemove = js.ValueOf("mousemove")
stringWheel = js.ValueOf("wheel")
stringTouchstart = js.ValueOf("touchstart")
stringTouchend = js.ValueOf("touchend")
stringTouchmove = js.ValueOf("touchmove")
)
type touchInClient struct {
id TouchID
x float64
y float64
}
func jsCodeToID(code js.Value) Key {
// js.Value cannot be used as a map key.
// As the number of keys is around 100, just a dumb loop should work.
for uiKey, jsCode := range uiKeyToJSCode {
if jsCode.Equal(code) {
return uiKey
}
}
return -1
}
var codeToMouseButton = map[int]MouseButton{
0: MouseButton0, // Left
1: MouseButton1, // Middle
2: MouseButton2, // Right
3: MouseButton3,
4: MouseButton4,
}
func eventToKeys(e js.Value) (key0, key1 Key, fromKeyProperty bool) {
id := jsCodeToID(e.Get("code"))
if id >= 0 {
return id, -1, false
}
// With a virtual keyboard on mobile devices, e.code is empty. Use a 'key' property instead (#2898).
key := e.Get("key")
// The key property doesn't distinghlish between left and right modifier keys.
// Let's assume both keys are pressed.
switch {
case key.Equal(stringAlt):
return KeyAltLeft, KeyAltRight, true
case key.Equal(stringControl):
return KeyControlLeft, KeyControlRight, true
case key.Equal(stringMeta):
return KeyMetaLeft, KeyMetaRight, true
case key.Equal(stringShift):
return KeyShiftLeft, KeyShiftRight, true
}
for uiKey, jsKey := range uiKeyToJSKey {
if key.Equal(jsKey) {
return uiKey, -1, true
}
}
return -1, -1, false
}
func (u *UserInterface) keyDown(event js.Value) {
key0, key1, fromKeyProperty := eventToKeys(event)
if key0 >= 0 {
// If the key value comes from a 'key' property, a 'keydown' and 'keyup' event might be fired too quickly.
// Record the key duration to prevent immediate resetting a key state by a 'keyup' event.
// Resetting a key state is delayed until the next tick. See updateInputState.
if fromKeyProperty && !u.inputState.KeyPressed[key0] {
if u.keyDurationsByKeyProperty == nil {
u.keyDurationsByKeyProperty = map[Key]int{}
}
u.keyDurationsByKeyProperty[key0] = 1
}
u.inputState.KeyPressed[key0] = true
}
if key1 >= 0 {
if fromKeyProperty && !u.inputState.KeyPressed[key1] {
if u.keyDurationsByKeyProperty == nil {
u.keyDurationsByKeyProperty = map[Key]int{}
}
u.keyDurationsByKeyProperty[key1] = 1
}
u.inputState.KeyPressed[key1] = true
}
}
func (u *UserInterface) keyUp(event js.Value) {
key0, key1, fromKeyProperty := eventToKeys(event)
if key0 >= 0 {
if !fromKeyProperty || u.keyDurationsByKeyProperty[key0] == 0 {
u.inputState.KeyPressed[key0] = false
}
}
if key1 >= 0 {
if !fromKeyProperty || u.keyDurationsByKeyProperty[key1] == 0 {
u.inputState.KeyPressed[key1] = false
}
}
}
func (u *UserInterface) mouseDown(code int) {
u.inputState.MouseButtonPressed[codeToMouseButton[code]] = true
}
func (u *UserInterface) mouseUp(code int) {
u.inputState.MouseButtonPressed[codeToMouseButton[code]] = false
}
func (u *UserInterface) updateInputFromEvent(e js.Value) error {
// Avoid using js.Value.String() as String creates a Uint8Array via a TextEncoder and causes a heavy
// overhead (#1437).
switch t := e.Get("type"); {
case t.Equal(stringKeydown):
if str := e.Get("key").String(); isKeyString(str) {
for _, r := range str {
u.inputState.appendRune(r)
}
}
u.keyDown(e)
case t.Equal(stringKeyup):
u.keyUp(e)
case t.Equal(stringMousedown):
u.mouseDown(e.Get("button").Int())
u.setMouseCursorFromEvent(e)
case t.Equal(stringMouseup):
u.mouseUp(e.Get("button").Int())
u.setMouseCursorFromEvent(e)
case t.Equal(stringMousemove):
u.setMouseCursorFromEvent(e)
case t.Equal(stringWheel):
// TODO: What if e.deltaMode is not DOM_DELTA_PIXEL?
u.inputState.WheelX = -e.Get("deltaX").Float()
u.inputState.WheelY = -e.Get("deltaY").Float()
case t.Equal(stringTouchstart) || t.Equal(stringTouchend) || t.Equal(stringTouchmove):
u.updateTouchesFromEvent(e)
}
u.forceUpdateOnMinimumFPSMode()
return nil
}
func (u *UserInterface) setMouseCursorFromEvent(e js.Value) {
if u.context == nil {
return
}
u.origCursorXInClient = e.Get("clientX").Float()
u.origCursorYInClient = e.Get("clientY").Float()
if u.cursorMode == CursorModeCaptured {
u.cursorXInClient += e.Get("movementX").Float()
u.cursorYInClient += e.Get("movementY").Float()
return
}
u.cursorXInClient = u.origCursorXInClient
u.cursorYInClient = u.origCursorYInClient
}
func (u *UserInterface) recoverCursorPosition() {
u.cursorXInClient = u.origCursorXInClient
u.cursorYInClient = u.origCursorYInClient
}
func (u *UserInterface) updateTouchesFromEvent(e js.Value) {
u.touchesInClient = u.touchesInClient[:0]
touches := e.Get("targetTouches")
for i := 0; i < touches.Length(); i++ {
t := touches.Call("item", i)
u.touchesInClient = append(u.touchesInClient, touchInClient{
id: TouchID(t.Get("identifier").Int()),
x: t.Get("clientX").Float(),
y: t.Get("clientY").Float(),
})
}
}
func isKeyString(str string) bool {
// From https://www.w3.org/TR/uievents-key/#keys-unicode,
//
// A key string is a string containing a 0 or 1 non-control characters
// ("base" characters) followed by 0 or more combining characters. The
// string MUST be in Normalized Form C (NFC) as described in
// [UnicodeNormalizationForms].
//
// A non-control character is any valid Unicode character except those
// that are part of the "Other, Control" ("Cc") General Category.
//
// A combining character is any valid Unicode character in the "Mark,
// Spacing Combining" ("Mc") General Category or with a non-zero
// Combining Class.
for i, r := range str {
if i == 0 {
if unicode.Is(unicode.Cc, r) {
return false
}
continue
}
if !unicode.Is(unicode.Mc, r) {
return false
}
}
return true
}
var (
jsKeyboard = js.Global().Get("navigator").Get("keyboard")
jsKeyboardGetLayoutMap js.Value
jsKeyboardGetLayoutMapCh chan js.Value
jsKeyboardGetLayoutMapCallback js.Func
)
func init() {
if !jsKeyboard.Truthy() {
return
}
jsKeyboardGetLayoutMap = jsKeyboard.Get("getLayoutMap").Call("bind", jsKeyboard)
jsKeyboardGetLayoutMapCh = make(chan js.Value, 1)
jsKeyboardGetLayoutMapCallback = js.FuncOf(func(this js.Value, args []js.Value) any {
jsKeyboardGetLayoutMapCh <- args[0]
return nil
})
}
func (u *UserInterface) KeyName(key Key) string {
if !u.isRunning() {
return ""
}
// keyboardLayoutMap is reset every tick.
if u.keyboardLayoutMap.IsUndefined() {
if !jsKeyboard.Truthy() {
return ""
}
// Invoke getLayoutMap every tick to detect the keyboard change.
// TODO: Calling this every tick might be inefficient. Is there a way to detect a keyboard change?
jsKeyboardGetLayoutMap.Invoke().Call("then", jsKeyboardGetLayoutMapCallback)
u.keyboardLayoutMap = <-jsKeyboardGetLayoutMapCh
}
n := u.keyboardLayoutMap.Call("get", uiKeyToJSCode[key])
if n.IsUndefined() {
return ""
}
return n.String()
}
func (u *UserInterface) UpdateInputFromEvent(e js.Value) {
u.updateInputFromEvent(e)
}
func (u *UserInterface) saveCursorPosition() {
u.savedCursorX = u.inputState.CursorX
u.savedCursorY = u.inputState.CursorY
w, h := u.outsideSize()
u.savedOutsideWidth = w
u.savedOutsideHeight = h
}
func (u *UserInterface) updateInputState() error {
// Reset the key state if a key is pressed by a 'key' property and the key's duration is big enough.
for key, duration := range u.keyDurationsByKeyProperty {
if duration >= 2 {
delete(u.keyDurationsByKeyProperty, key)
u.inputState.KeyPressed[key] = false
continue
}
u.keyDurationsByKeyProperty[key]++
}
s := theMonitor.DeviceScaleFactor()
if !math.IsNaN(u.savedCursorX) && !math.IsNaN(u.savedCursorY) {
// If savedCursorX and savedCursorY are valid values, the cursor is saved just before entering or exiting from fullscreen.
// Even after entering or exiting from fullscreening, the outside (body) size is not updated for a while.
// Wait for the outside size updated.
if w, h := u.outsideSize(); u.savedOutsideWidth != w || u.savedOutsideHeight != h {
u.inputState.CursorX = u.savedCursorX
u.inputState.CursorY = u.savedCursorY
cx, cy := u.context.logicalPositionToClientPosition(u.inputState.CursorX, u.inputState.CursorY, s)
u.cursorXInClient = cx
u.cursorYInClient = cy
u.savedCursorX = math.NaN()
u.savedCursorY = math.NaN()
u.savedOutsideWidth = 0
u.savedOutsideHeight = 0
u.outsideSizeUnchangedCount = 0
} else {
u.outsideSizeUnchangedCount++
// If the outside size is not changed for a while, probably the screen size is not actually changed.
// Reset the state.
if u.outsideSizeUnchangedCount > 60 {
u.savedCursorX = math.NaN()
u.savedCursorY = math.NaN()
u.savedOutsideWidth = 0
u.savedOutsideHeight = 0
u.outsideSizeUnchangedCount = 0
}
}
} else {
cx, cy := u.context.clientPositionToLogicalPosition(u.cursorXInClient, u.cursorYInClient, s)
u.inputState.CursorX = cx
u.inputState.CursorY = cy
}
u.inputState.Touches = u.inputState.Touches[:0]
for _, t := range u.touchesInClient {
x, y := u.context.clientPositionToLogicalPosition(t.x, t.y, s)
u.inputState.Touches = append(u.inputState.Touches, Touch{
ID: t.id,
X: int(x),
Y: int(y),
})
}
return nil
}
// uiKeyToJSKey is a map from Key values to KeyboardEvent's key values.
// Note that js.Value cannot be a map key.
//
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
var uiKeyToJSKey = map[Key]js.Value{
KeyCapsLock: js.ValueOf("CapsLock"),
KeyNumLock: js.ValueOf("NumLock"),
KeyScrollLock: js.ValueOf("ScrollLock"),
KeyEnter: js.ValueOf("Enter"),
KeyTab: js.ValueOf("Tab"),
KeySpace: js.ValueOf(" "),
KeyArrowDown: js.ValueOf("ArrowDown"),
KeyArrowLeft: js.ValueOf("ArrowLeft"),
KeyArrowRight: js.ValueOf("ArrowRight"),
KeyArrowUp: js.ValueOf("ArrowUp"),
KeyEnd: js.ValueOf("End"),
KeyHome: js.ValueOf("Home"),
KeyPageDown: js.ValueOf("PageDown"),
KeyPageUp: js.ValueOf("PageUp"),
KeyBackspace: js.ValueOf("Backspace"),
KeyDelete: js.ValueOf("Delete"),
KeyInsert: js.ValueOf("Insert"),
KeyContextMenu: js.ValueOf("ContextMenu"),
KeyEscape: js.ValueOf("Escape"),
KeyPause: js.ValueOf("Pause"),
KeyPrintScreen: js.ValueOf("PrintScreen"),
KeyF1: js.ValueOf("F1"),
KeyF2: js.ValueOf("F2"),
KeyF3: js.ValueOf("F3"),
KeyF4: js.ValueOf("F4"),
KeyF5: js.ValueOf("F5"),
KeyF6: js.ValueOf("F6"),
KeyF7: js.ValueOf("F7"),
KeyF8: js.ValueOf("F8"),
KeyF9: js.ValueOf("F9"),
KeyF10: js.ValueOf("F10"),
KeyF11: js.ValueOf("F11"),
KeyF12: js.ValueOf("F12"),
KeyF13: js.ValueOf("F13"),
KeyF14: js.ValueOf("F14"),
KeyF15: js.ValueOf("F15"),
KeyF16: js.ValueOf("F16"),
KeyF17: js.ValueOf("F17"),
KeyF18: js.ValueOf("F18"),
KeyF19: js.ValueOf("F19"),
KeyF20: js.ValueOf("F20"),
KeyNumpadDecimal: js.ValueOf("Decimal"),
KeyNumpadMultiply: js.ValueOf("Multiply"),
KeyNumpadAdd: js.ValueOf("Add"),
KeyNumpadDivide: js.ValueOf("Divide"),
KeyNumpadSubtract: js.ValueOf("Subtract"),
KeyNumpad0: js.ValueOf("0"),
KeyNumpad1: js.ValueOf("1"),
KeyNumpad2: js.ValueOf("2"),
KeyNumpad3: js.ValueOf("3"),
KeyNumpad4: js.ValueOf("4"),
KeyNumpad5: js.ValueOf("5"),
KeyNumpad6: js.ValueOf("6"),
KeyNumpad7: js.ValueOf("7"),
KeyNumpad8: js.ValueOf("8"),
KeyNumpad9: js.ValueOf("9"),
}
func (i *InputState) resetForBlur() {
for j := range i.KeyPressed {
i.KeyPressed[j] = false
}
for j := range i.MouseButtonPressed {
i.MouseButtonPressed[j] = false
}
i.Touches = i.Touches[:0]
}
+67
View File
@@ -0,0 +1,67 @@
// Copyright 2016 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.
//go:build android || ios
package ui
type TouchForInput struct {
ID TouchID
// X is in device-independent pixels.
X float64
// Y is in device-independent pixels.
Y float64
}
func (u *UserInterface) updateInputStateFromOutside(keys map[Key]struct{}, runes []rune, touches []TouchForInput) {
u.m.Lock()
defer u.m.Unlock()
for k := range u.inputState.KeyPressed {
_, ok := keys[Key(k)]
u.inputState.KeyPressed[k] = ok
}
u.inputState.Runes = append(u.inputState.Runes, runes...)
u.touches = u.touches[:0]
for _, t := range touches {
u.touches = append(u.touches, t)
}
}
func (u *UserInterface) updateInputState() error {
u.m.Lock()
defer u.m.Unlock()
s := theMonitor.DeviceScaleFactor()
u.inputState.Touches = u.inputState.Touches[:0]
for _, t := range u.touches {
x, y := u.context.clientPositionToLogicalPosition(t.X, t.Y, s)
u.inputState.Touches = append(u.inputState.Touches, Touch{
ID: t.ID,
X: int(x),
Y: int(y),
})
}
return nil
}
func (u *UserInterface) KeyName(key Key) string {
// TODO: Implement this.
return ""
}
@@ -0,0 +1,25 @@
// Copyright 2023 The Ebitengine Authors
//
// 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.
//go:build nintendosdk
// The actual implementaiton will be provided by -overlay.
#include "input_nintendosdk.h"
extern "C" void ebitengine_UpdateTouches() {}
extern "C" int ebitengine_GetTouchCount() { return 0; }
extern "C" void ebitengine_GetTouches(struct Touch *touches) {}
@@ -0,0 +1,76 @@
// Copyright 2021 The Ebiten Authors
//
// 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.
//go:build nintendosdk
package ui
// #cgo !darwin LDFLAGS: -Wl,-unresolved-symbols=ignore-all
// #cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup
//
// #include "input_nintendosdk.h"
//
// const int kScreenWidth = 1920;
// const int kScreenHeight = 1080;
import "C"
import (
"github.com/hajimehoshi/ebiten/v2/internal/gamepad"
)
func (u *UserInterface) updateInputState() error {
var err error
u.mainThread.Call(func() {
err = u.updateInputStateImpl()
})
return err
}
// updateInputStateImpl must be called from the main thread.
func (u *UserInterface) updateInputStateImpl() error {
if err := gamepad.Update(); err != nil {
return err
}
C.ebitengine_UpdateTouches()
u.nativeTouches = u.nativeTouches[:0]
if n := int(C.ebitengine_GetTouchCount()); n > 0 {
if cap(u.nativeTouches) < n {
u.nativeTouches = make([]C.struct_Touch, n)
} else {
u.nativeTouches = u.nativeTouches[:n]
}
C.ebitengine_GetTouches(&u.nativeTouches[0])
}
u.m.Lock()
defer u.m.Unlock()
u.inputState.Touches = u.inputState.Touches[:0]
for _, t := range u.nativeTouches {
x, y := u.context.clientPositionToLogicalPosition(float64(t.x), float64(t.y), theMonitor.DeviceScaleFactor())
u.inputState.Touches = append(u.inputState.Touches, Touch{
ID: TouchID(t.id),
X: int(x),
Y: int(y),
})
}
return nil
}
func (u *UserInterface) KeyName(key Key) string {
return ""
}
@@ -0,0 +1,36 @@
// Copyright 2023 The Ebitengine Authors
//
// 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.
//go:build nintendosdk
struct Touch {
int id;
int x;
int y;
};
extern const int kScreenWidth;
extern const int kScreenHeight;
#ifdef __cplusplus
extern "C" {
#endif
void ebitengine_UpdateTouches();
int ebitengine_GetTouchCount();
void ebitengine_GetTouches(struct Touch* touches);
#ifdef __cplusplus
} // extern "C"
#endif
@@ -0,0 +1,26 @@
// Copyright 2023 The Ebitengine Authors
//
// 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.
//go:build playstation5
package ui
func (u *UserInterface) updateInputState() error {
// TODO: Implement this
return nil
}
func (u *UserInterface) KeyName(key Key) string {
return ""
}
+391
View File
@@ -0,0 +1,391 @@
// Copyright 2013 The Ebitengine Authors
//
// 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.
// Code generated by genkeys.go using 'go generate'. DO NOT EDIT.
package ui
import (
"fmt"
)
type Key int
const (
KeyA Key = iota
KeyB
KeyC
KeyD
KeyE
KeyF
KeyG
KeyH
KeyI
KeyJ
KeyK
KeyL
KeyM
KeyN
KeyO
KeyP
KeyQ
KeyR
KeyS
KeyT
KeyU
KeyV
KeyW
KeyX
KeyY
KeyZ
KeyAltLeft
KeyAltRight
KeyArrowDown
KeyArrowLeft
KeyArrowRight
KeyArrowUp
KeyBackquote
KeyBackslash
KeyBackspace
KeyBracketLeft
KeyBracketRight
KeyCapsLock
KeyComma
KeyContextMenu
KeyControlLeft
KeyControlRight
KeyDelete
KeyDigit0
KeyDigit1
KeyDigit2
KeyDigit3
KeyDigit4
KeyDigit5
KeyDigit6
KeyDigit7
KeyDigit8
KeyDigit9
KeyEnd
KeyEnter
KeyEqual
KeyEscape
KeyF1
KeyF2
KeyF3
KeyF4
KeyF5
KeyF6
KeyF7
KeyF8
KeyF9
KeyF10
KeyF11
KeyF12
KeyF13
KeyF14
KeyF15
KeyF16
KeyF17
KeyF18
KeyF19
KeyF20
KeyF21
KeyF22
KeyF23
KeyF24
KeyHome
KeyInsert
KeyIntlBackslash
KeyMetaLeft
KeyMetaRight
KeyMinus
KeyNumLock
KeyNumpad0
KeyNumpad1
KeyNumpad2
KeyNumpad3
KeyNumpad4
KeyNumpad5
KeyNumpad6
KeyNumpad7
KeyNumpad8
KeyNumpad9
KeyNumpadAdd
KeyNumpadDecimal
KeyNumpadDivide
KeyNumpadEnter
KeyNumpadEqual
KeyNumpadMultiply
KeyNumpadSubtract
KeyPageDown
KeyPageUp
KeyPause
KeyPeriod
KeyPrintScreen
KeyQuote
KeyScrollLock
KeySemicolon
KeyShiftLeft
KeyShiftRight
KeySlash
KeySpace
KeyTab
KeyReserved0
KeyReserved1
KeyReserved2
KeyReserved3
KeyMax = KeyReserved3
)
func (k Key) String() string {
switch k {
case KeyA:
return "KeyA"
case KeyB:
return "KeyB"
case KeyC:
return "KeyC"
case KeyD:
return "KeyD"
case KeyE:
return "KeyE"
case KeyF:
return "KeyF"
case KeyG:
return "KeyG"
case KeyH:
return "KeyH"
case KeyI:
return "KeyI"
case KeyJ:
return "KeyJ"
case KeyK:
return "KeyK"
case KeyL:
return "KeyL"
case KeyM:
return "KeyM"
case KeyN:
return "KeyN"
case KeyO:
return "KeyO"
case KeyP:
return "KeyP"
case KeyQ:
return "KeyQ"
case KeyR:
return "KeyR"
case KeyS:
return "KeyS"
case KeyT:
return "KeyT"
case KeyU:
return "KeyU"
case KeyV:
return "KeyV"
case KeyW:
return "KeyW"
case KeyX:
return "KeyX"
case KeyY:
return "KeyY"
case KeyZ:
return "KeyZ"
case KeyAltLeft:
return "KeyAltLeft"
case KeyAltRight:
return "KeyAltRight"
case KeyArrowDown:
return "KeyArrowDown"
case KeyArrowLeft:
return "KeyArrowLeft"
case KeyArrowRight:
return "KeyArrowRight"
case KeyArrowUp:
return "KeyArrowUp"
case KeyBackquote:
return "KeyBackquote"
case KeyBackslash:
return "KeyBackslash"
case KeyBackspace:
return "KeyBackspace"
case KeyBracketLeft:
return "KeyBracketLeft"
case KeyBracketRight:
return "KeyBracketRight"
case KeyCapsLock:
return "KeyCapsLock"
case KeyComma:
return "KeyComma"
case KeyContextMenu:
return "KeyContextMenu"
case KeyControlLeft:
return "KeyControlLeft"
case KeyControlRight:
return "KeyControlRight"
case KeyDelete:
return "KeyDelete"
case KeyDigit0:
return "KeyDigit0"
case KeyDigit1:
return "KeyDigit1"
case KeyDigit2:
return "KeyDigit2"
case KeyDigit3:
return "KeyDigit3"
case KeyDigit4:
return "KeyDigit4"
case KeyDigit5:
return "KeyDigit5"
case KeyDigit6:
return "KeyDigit6"
case KeyDigit7:
return "KeyDigit7"
case KeyDigit8:
return "KeyDigit8"
case KeyDigit9:
return "KeyDigit9"
case KeyEnd:
return "KeyEnd"
case KeyEnter:
return "KeyEnter"
case KeyEqual:
return "KeyEqual"
case KeyEscape:
return "KeyEscape"
case KeyF1:
return "KeyF1"
case KeyF2:
return "KeyF2"
case KeyF3:
return "KeyF3"
case KeyF4:
return "KeyF4"
case KeyF5:
return "KeyF5"
case KeyF6:
return "KeyF6"
case KeyF7:
return "KeyF7"
case KeyF8:
return "KeyF8"
case KeyF9:
return "KeyF9"
case KeyF10:
return "KeyF10"
case KeyF11:
return "KeyF11"
case KeyF12:
return "KeyF12"
case KeyF13:
return "KeyF13"
case KeyF14:
return "KeyF14"
case KeyF15:
return "KeyF15"
case KeyF16:
return "KeyF16"
case KeyF17:
return "KeyF17"
case KeyF18:
return "KeyF18"
case KeyF19:
return "KeyF19"
case KeyF20:
return "KeyF20"
case KeyF21:
return "KeyF21"
case KeyF22:
return "KeyF22"
case KeyF23:
return "KeyF23"
case KeyF24:
return "KeyF24"
case KeyHome:
return "KeyHome"
case KeyInsert:
return "KeyInsert"
case KeyIntlBackslash:
return "KeyIntlBackslash"
case KeyMetaLeft:
return "KeyMetaLeft"
case KeyMetaRight:
return "KeyMetaRight"
case KeyMinus:
return "KeyMinus"
case KeyNumLock:
return "KeyNumLock"
case KeyNumpad0:
return "KeyNumpad0"
case KeyNumpad1:
return "KeyNumpad1"
case KeyNumpad2:
return "KeyNumpad2"
case KeyNumpad3:
return "KeyNumpad3"
case KeyNumpad4:
return "KeyNumpad4"
case KeyNumpad5:
return "KeyNumpad5"
case KeyNumpad6:
return "KeyNumpad6"
case KeyNumpad7:
return "KeyNumpad7"
case KeyNumpad8:
return "KeyNumpad8"
case KeyNumpad9:
return "KeyNumpad9"
case KeyNumpadAdd:
return "KeyNumpadAdd"
case KeyNumpadDecimal:
return "KeyNumpadDecimal"
case KeyNumpadDivide:
return "KeyNumpadDivide"
case KeyNumpadEnter:
return "KeyNumpadEnter"
case KeyNumpadEqual:
return "KeyNumpadEqual"
case KeyNumpadMultiply:
return "KeyNumpadMultiply"
case KeyNumpadSubtract:
return "KeyNumpadSubtract"
case KeyPageDown:
return "KeyPageDown"
case KeyPageUp:
return "KeyPageUp"
case KeyPause:
return "KeyPause"
case KeyPeriod:
return "KeyPeriod"
case KeyPrintScreen:
return "KeyPrintScreen"
case KeyQuote:
return "KeyQuote"
case KeyScrollLock:
return "KeyScrollLock"
case KeySemicolon:
return "KeySemicolon"
case KeyShiftLeft:
return "KeyShiftLeft"
case KeyShiftRight:
return "KeyShiftRight"
case KeySlash:
return "KeySlash"
case KeySpace:
return "KeySpace"
case KeyTab:
return "KeyTab"
}
return fmt.Sprintf("Key(%d)", k)
}
+144
View File
@@ -0,0 +1,144 @@
// Copyright 2013 The Ebitengine Authors
//
// 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.
// Code generated by genkeys.go using 'go generate'. DO NOT EDIT.
//go:build !android && !ios && !js && !nintendosdk && !playstation5
package ui
import (
"github.com/hajimehoshi/ebiten/v2/internal/glfw"
)
var uiKeyToGLFWKey = map[Key]glfw.Key{
KeyA: glfw.KeyA,
KeyAltLeft: glfw.KeyLeftAlt,
KeyAltRight: glfw.KeyRightAlt,
KeyArrowDown: glfw.KeyDown,
KeyArrowLeft: glfw.KeyLeft,
KeyArrowRight: glfw.KeyRight,
KeyArrowUp: glfw.KeyUp,
KeyB: glfw.KeyB,
KeyBackquote: glfw.KeyGraveAccent,
KeyBackslash: glfw.KeyBackslash,
KeyBackspace: glfw.KeyBackspace,
KeyBracketLeft: glfw.KeyLeftBracket,
KeyBracketRight: glfw.KeyRightBracket,
KeyC: glfw.KeyC,
KeyCapsLock: glfw.KeyCapsLock,
KeyComma: glfw.KeyComma,
KeyContextMenu: glfw.KeyMenu,
KeyControlLeft: glfw.KeyLeftControl,
KeyControlRight: glfw.KeyRightControl,
KeyD: glfw.KeyD,
KeyDelete: glfw.KeyDelete,
KeyDigit0: glfw.Key0,
KeyDigit1: glfw.Key1,
KeyDigit2: glfw.Key2,
KeyDigit3: glfw.Key3,
KeyDigit4: glfw.Key4,
KeyDigit5: glfw.Key5,
KeyDigit6: glfw.Key6,
KeyDigit7: glfw.Key7,
KeyDigit8: glfw.Key8,
KeyDigit9: glfw.Key9,
KeyE: glfw.KeyE,
KeyEnd: glfw.KeyEnd,
KeyEnter: glfw.KeyEnter,
KeyEqual: glfw.KeyEqual,
KeyEscape: glfw.KeyEscape,
KeyF: glfw.KeyF,
KeyF1: glfw.KeyF1,
KeyF10: glfw.KeyF10,
KeyF11: glfw.KeyF11,
KeyF12: glfw.KeyF12,
KeyF13: glfw.KeyF13,
KeyF14: glfw.KeyF14,
KeyF15: glfw.KeyF15,
KeyF16: glfw.KeyF16,
KeyF17: glfw.KeyF17,
KeyF18: glfw.KeyF18,
KeyF19: glfw.KeyF19,
KeyF2: glfw.KeyF2,
KeyF20: glfw.KeyF20,
KeyF21: glfw.KeyF21,
KeyF22: glfw.KeyF22,
KeyF23: glfw.KeyF23,
KeyF24: glfw.KeyF24,
KeyF3: glfw.KeyF3,
KeyF4: glfw.KeyF4,
KeyF5: glfw.KeyF5,
KeyF6: glfw.KeyF6,
KeyF7: glfw.KeyF7,
KeyF8: glfw.KeyF8,
KeyF9: glfw.KeyF9,
KeyG: glfw.KeyG,
KeyH: glfw.KeyH,
KeyHome: glfw.KeyHome,
KeyI: glfw.KeyI,
KeyInsert: glfw.KeyInsert,
KeyIntlBackslash: glfw.KeyWorld1,
KeyJ: glfw.KeyJ,
KeyK: glfw.KeyK,
KeyL: glfw.KeyL,
KeyM: glfw.KeyM,
KeyMetaLeft: glfw.KeyLeftSuper,
KeyMetaRight: glfw.KeyRightSuper,
KeyMinus: glfw.KeyMinus,
KeyN: glfw.KeyN,
KeyNumLock: glfw.KeyNumLock,
KeyNumpad0: glfw.KeyKP0,
KeyNumpad1: glfw.KeyKP1,
KeyNumpad2: glfw.KeyKP2,
KeyNumpad3: glfw.KeyKP3,
KeyNumpad4: glfw.KeyKP4,
KeyNumpad5: glfw.KeyKP5,
KeyNumpad6: glfw.KeyKP6,
KeyNumpad7: glfw.KeyKP7,
KeyNumpad8: glfw.KeyKP8,
KeyNumpad9: glfw.KeyKP9,
KeyNumpadAdd: glfw.KeyKPAdd,
KeyNumpadDecimal: glfw.KeyKPDecimal,
KeyNumpadDivide: glfw.KeyKPDivide,
KeyNumpadEnter: glfw.KeyKPEnter,
KeyNumpadEqual: glfw.KeyKPEqual,
KeyNumpadMultiply: glfw.KeyKPMultiply,
KeyNumpadSubtract: glfw.KeyKPSubtract,
KeyO: glfw.KeyO,
KeyP: glfw.KeyP,
KeyPageDown: glfw.KeyPageDown,
KeyPageUp: glfw.KeyPageUp,
KeyPause: glfw.KeyPause,
KeyPeriod: glfw.KeyPeriod,
KeyPrintScreen: glfw.KeyPrintScreen,
KeyQ: glfw.KeyQ,
KeyQuote: glfw.KeyApostrophe,
KeyR: glfw.KeyR,
KeyS: glfw.KeyS,
KeyScrollLock: glfw.KeyScrollLock,
KeySemicolon: glfw.KeySemicolon,
KeyShiftLeft: glfw.KeyLeftShift,
KeyShiftRight: glfw.KeyRightShift,
KeySlash: glfw.KeySlash,
KeySpace: glfw.KeySpace,
KeyT: glfw.KeyT,
KeyTab: glfw.KeyTab,
KeyU: glfw.KeyU,
KeyV: glfw.KeyV,
KeyW: glfw.KeyW,
KeyX: glfw.KeyX,
KeyY: glfw.KeyY,
KeyZ: glfw.KeyZ,
}
+142
View File
@@ -0,0 +1,142 @@
// Copyright 2013 The Ebitengine Authors
//
// 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.
// Code generated by genkeys.go using 'go generate'. DO NOT EDIT.
package ui
import (
"syscall/js"
)
var uiKeyToJSCode = map[Key]js.Value{
KeyA: js.ValueOf("KeyA"),
KeyAltLeft: js.ValueOf("AltLeft"),
KeyAltRight: js.ValueOf("AltRight"),
KeyArrowDown: js.ValueOf("ArrowDown"),
KeyArrowLeft: js.ValueOf("ArrowLeft"),
KeyArrowRight: js.ValueOf("ArrowRight"),
KeyArrowUp: js.ValueOf("ArrowUp"),
KeyB: js.ValueOf("KeyB"),
KeyBackquote: js.ValueOf("Backquote"),
KeyBackslash: js.ValueOf("Backslash"),
KeyBackspace: js.ValueOf("Backspace"),
KeyBracketLeft: js.ValueOf("BracketLeft"),
KeyBracketRight: js.ValueOf("BracketRight"),
KeyC: js.ValueOf("KeyC"),
KeyCapsLock: js.ValueOf("CapsLock"),
KeyComma: js.ValueOf("Comma"),
KeyContextMenu: js.ValueOf("ContextMenu"),
KeyControlLeft: js.ValueOf("ControlLeft"),
KeyControlRight: js.ValueOf("ControlRight"),
KeyD: js.ValueOf("KeyD"),
KeyDelete: js.ValueOf("Delete"),
KeyDigit0: js.ValueOf("Digit0"),
KeyDigit1: js.ValueOf("Digit1"),
KeyDigit2: js.ValueOf("Digit2"),
KeyDigit3: js.ValueOf("Digit3"),
KeyDigit4: js.ValueOf("Digit4"),
KeyDigit5: js.ValueOf("Digit5"),
KeyDigit6: js.ValueOf("Digit6"),
KeyDigit7: js.ValueOf("Digit7"),
KeyDigit8: js.ValueOf("Digit8"),
KeyDigit9: js.ValueOf("Digit9"),
KeyE: js.ValueOf("KeyE"),
KeyEnd: js.ValueOf("End"),
KeyEnter: js.ValueOf("Enter"),
KeyEqual: js.ValueOf("Equal"),
KeyEscape: js.ValueOf("Escape"),
KeyF: js.ValueOf("KeyF"),
KeyF1: js.ValueOf("F1"),
KeyF10: js.ValueOf("F10"),
KeyF11: js.ValueOf("F11"),
KeyF12: js.ValueOf("F12"),
KeyF13: js.ValueOf("F13"),
KeyF14: js.ValueOf("F14"),
KeyF15: js.ValueOf("F15"),
KeyF16: js.ValueOf("F16"),
KeyF17: js.ValueOf("F17"),
KeyF18: js.ValueOf("F18"),
KeyF19: js.ValueOf("F19"),
KeyF2: js.ValueOf("F2"),
KeyF20: js.ValueOf("F20"),
KeyF21: js.ValueOf("F21"),
KeyF22: js.ValueOf("F22"),
KeyF23: js.ValueOf("F23"),
KeyF24: js.ValueOf("F24"),
KeyF3: js.ValueOf("F3"),
KeyF4: js.ValueOf("F4"),
KeyF5: js.ValueOf("F5"),
KeyF6: js.ValueOf("F6"),
KeyF7: js.ValueOf("F7"),
KeyF8: js.ValueOf("F8"),
KeyF9: js.ValueOf("F9"),
KeyG: js.ValueOf("KeyG"),
KeyH: js.ValueOf("KeyH"),
KeyHome: js.ValueOf("Home"),
KeyI: js.ValueOf("KeyI"),
KeyInsert: js.ValueOf("Insert"),
KeyIntlBackslash: js.ValueOf("IntlBackslash"),
KeyJ: js.ValueOf("KeyJ"),
KeyK: js.ValueOf("KeyK"),
KeyL: js.ValueOf("KeyL"),
KeyM: js.ValueOf("KeyM"),
KeyMetaLeft: js.ValueOf("MetaLeft"),
KeyMetaRight: js.ValueOf("MetaRight"),
KeyMinus: js.ValueOf("Minus"),
KeyN: js.ValueOf("KeyN"),
KeyNumLock: js.ValueOf("NumLock"),
KeyNumpad0: js.ValueOf("Numpad0"),
KeyNumpad1: js.ValueOf("Numpad1"),
KeyNumpad2: js.ValueOf("Numpad2"),
KeyNumpad3: js.ValueOf("Numpad3"),
KeyNumpad4: js.ValueOf("Numpad4"),
KeyNumpad5: js.ValueOf("Numpad5"),
KeyNumpad6: js.ValueOf("Numpad6"),
KeyNumpad7: js.ValueOf("Numpad7"),
KeyNumpad8: js.ValueOf("Numpad8"),
KeyNumpad9: js.ValueOf("Numpad9"),
KeyNumpadAdd: js.ValueOf("NumpadAdd"),
KeyNumpadDecimal: js.ValueOf("NumpadDecimal"),
KeyNumpadDivide: js.ValueOf("NumpadDivide"),
KeyNumpadEnter: js.ValueOf("NumpadEnter"),
KeyNumpadEqual: js.ValueOf("NumpadEqual"),
KeyNumpadMultiply: js.ValueOf("NumpadMultiply"),
KeyNumpadSubtract: js.ValueOf("NumpadSubtract"),
KeyO: js.ValueOf("KeyO"),
KeyP: js.ValueOf("KeyP"),
KeyPageDown: js.ValueOf("PageDown"),
KeyPageUp: js.ValueOf("PageUp"),
KeyPause: js.ValueOf("Pause"),
KeyPeriod: js.ValueOf("Period"),
KeyPrintScreen: js.ValueOf("PrintScreen"),
KeyQ: js.ValueOf("KeyQ"),
KeyQuote: js.ValueOf("Quote"),
KeyR: js.ValueOf("KeyR"),
KeyS: js.ValueOf("KeyS"),
KeyScrollLock: js.ValueOf("ScrollLock"),
KeySemicolon: js.ValueOf("Semicolon"),
KeyShiftLeft: js.ValueOf("ShiftLeft"),
KeyShiftRight: js.ValueOf("ShiftRight"),
KeySlash: js.ValueOf("Slash"),
KeySpace: js.ValueOf("Space"),
KeyT: js.ValueOf("KeyT"),
KeyTab: js.ValueOf("Tab"),
KeyU: js.ValueOf("KeyU"),
KeyV: js.ValueOf("KeyV"),
KeyW: js.ValueOf("KeyW"),
KeyX: js.ValueOf("KeyX"),
KeyY: js.ValueOf("KeyY"),
KeyZ: js.ValueOf("KeyZ"),
}
+184
View File
@@ -0,0 +1,184 @@
// Copyright 2023 The Ebitengine Authors
//
// 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.
//go:build !android && !ios && !js && !nintendosdk && !playstation5
package ui
import (
"image"
"sync"
"sync/atomic"
"github.com/hajimehoshi/ebiten/v2/internal/glfw"
)
// Monitor is a wrapper around glfw.Monitor.
type Monitor struct {
m *glfw.Monitor
videoMode *glfw.VidMode
id int
name string
boundsInGLFWPixels image.Rectangle
contentScale float64
}
// Name returns the monitor's name.
func (m *Monitor) Name() string {
return m.name
}
// DeviceScaleFactor is concurrent-safe as contentScale is immutable.
func (m *Monitor) DeviceScaleFactor() float64 {
return m.contentScale
}
// Size returns the size of the monitor in device-independent pixels.
func (m *Monitor) Size() (int, int) {
w, h := m.sizeInDIP()
return int(w), int(h)
}
func (m *Monitor) sizeInDIP() (float64, float64) {
w, h := m.boundsInGLFWPixels.Dx(), m.boundsInGLFWPixels.Dy()
s := m.DeviceScaleFactor()
return dipFromGLFWPixel(float64(w), s), dipFromGLFWPixel(float64(h), s)
}
type monitors struct {
// monitors is the monitor list cache for desktop glfw compile targets.
// populated by 'updateMonitors' which is called on init and every
// monitor config change event.
monitors []*Monitor
m sync.Mutex
updateCalled int32
}
var theMonitors monitors
func (m *monitors) append(ms []*Monitor) []*Monitor {
if atomic.LoadInt32(&m.updateCalled) == 0 {
panic("ui: (*monitors).update must be called before (*monitors).append is called")
}
m.m.Lock()
defer m.m.Unlock()
return append(ms, m.monitors...)
}
func (m *monitors) primaryMonitor() *Monitor {
if atomic.LoadInt32(&m.updateCalled) == 0 {
panic("ui: (*monitors).update must be called before (*monitors).primaryMonitor is called")
}
m.m.Lock()
defer m.m.Unlock()
// GetMonitors might return nil in theory (#1878, #1887).
// primaryMonitor can be called at the initialization, so monitors can be nil.
if len(m.monitors) == 0 {
return nil
}
return m.monitors[0]
}
func (m *monitors) monitorFromID(id int) *Monitor {
m.m.Lock()
defer m.m.Unlock()
return m.monitors[id]
}
// monitorFromPosition returns a monitor for the given position (x, y),
// or returns nil if monitor is not found.
// The position is in GLFW pixels.
func (m *monitors) monitorFromPosition(x, y int) *Monitor {
m.m.Lock()
defer m.m.Unlock()
for _, m := range m.monitors {
// Use an inclusive range. On macOS, the cursor position can take this range (#2794).
b := m.boundsInGLFWPixels
if b.Min.X <= x && x <= b.Max.X && b.Min.Y <= y && y <= b.Max.Y {
return m
}
}
return nil
}
// update must be called from the main thread.
func (m *monitors) update() error {
glfwMonitors, err := glfw.GetMonitors()
if err != nil {
return err
}
newMonitors := make([]*Monitor, 0, len(glfwMonitors))
for i, m := range glfwMonitors {
x, y, err := m.GetPos()
if err != nil {
return err
}
// TODO: Detect the update of the content scale by SetContentScaleCallback (#2343).
contentScale := 1.0
// Keep calling GetContentScale until the returned scale is 0 (#2051).
// Retry this at most 5 times to avoid an infinite loop.
for i := 0; i < 5; i++ {
// An error can happen e.g. when entering a screensaver on Windows (#2488).
sx, _, err := m.GetContentScale()
if err != nil {
continue
}
if sx == 0 {
continue
}
contentScale = float64(sx)
break
}
videoMode, err := m.GetVideoMode()
if err != nil {
return err
}
name, err := m.GetName()
if err != nil {
return err
}
w, h, err := glfwMonitorSizeInGLFWPixels(m)
if err != nil {
return err
}
b := image.Rect(x, y, x+w, y+h)
newMonitors = append(newMonitors, &Monitor{
m: m,
videoMode: videoMode,
id: i,
name: name,
boundsInGLFWPixels: b,
contentScale: contentScale,
})
}
m.m.Lock()
m.monitors = newMonitors
m.m.Unlock()
atomic.StoreInt32(&m.updateCalled, 1)
return nil
}
@@ -0,0 +1,23 @@
// Copyright 2023 The Ebitengine Authors
//
// 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.
//go:build nintendosdk && !nintendosdkprofile
package ui
func initializeProfiler() {
}
func recordProfilerHeartbeat() {
}
@@ -0,0 +1,28 @@
// Copyright 2023 The Ebitengine Authors
//
// 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.
//go:build nintendosdk && nintendosdkprofile
package ui
// #include "init_nintendosdk.h"
import "C"
func initializeProfiler() {
C.ebitengine_InitializeProfiler()
}
func recordProfilerHeartbeat() {
C.ebitengine_RecordProfilerHeartbeat()
}
+93
View File
@@ -0,0 +1,93 @@
// Copyright 2023 The Ebitengine Authors
//
// 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.
//go:build !android && !ios
package ui
import (
stdcontext "context"
"runtime"
"golang.org/x/sync/errgroup"
"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
"github.com/hajimehoshi/ebiten/v2/internal/thread"
)
func (u *UserInterface) Run(game Game, options *RunOptions) error {
if options.SingleThread || buildTagSingleThread || runtime.GOOS == "js" {
return u.runSingleThread(game, options)
}
return u.runMultiThread(game, options)
}
func (u *UserInterface) runMultiThread(game Game, options *RunOptions) error {
u.mainThread = thread.NewOSThread()
graphicscommand.SetOSThreadAsRenderThread()
// Set the running state true after the main thread is set, and before initOnMainThread is called (#2742).
// TODO: As the existence of the main thread is the same as the value of `running`, this is redundant.
// Make `mainThread` atomic and remove `running` if possible.
u.setRunning(true)
defer u.setRunning(false)
u.context = newContext(game)
if err := u.initOnMainThread(options); err != nil {
return err
}
ctx, cancel := stdcontext.WithCancel(stdcontext.Background())
defer cancel()
var wg errgroup.Group
// Run the render thread.
wg.Go(func() error {
defer cancel()
graphicscommand.LoopRenderThread(ctx)
return nil
})
// Run the game thread.
wg.Go(func() error {
defer cancel()
return u.loopGame()
})
// Run the main thread.
_ = u.mainThread.Loop(ctx)
return wg.Wait()
}
func (u *UserInterface) runSingleThread(game Game, options *RunOptions) error {
// Initialize the main thread first so the thread is available at u.run (#809).
u.mainThread = thread.NewNoopThread()
u.setRunning(true)
defer u.setRunning(false)
u.context = newContext(game)
if err := u.initOnMainThread(options); err != nil {
return err
}
if err := u.loopGame(); err != nil {
return err
}
return nil
}
@@ -0,0 +1,19 @@
// Copyright 2020 The Ebiten Authors
//
// 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.
//go:build !ebitenginesinglethread && !ebitensinglethread
package ui
const buildTagSingleThread = false
@@ -0,0 +1,19 @@
// Copyright 2020 The Ebiten Authors
//
// 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.
//go:build ebitenginesinglethread || ebitensinglethread
package ui
const buildTagSingleThread = true
@@ -0,0 +1,20 @@
// Copyright 2024 The Ebitengine Authors
//
// 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.
package ui
func (u *UserInterface) ScreenSizeInFullscreen() (int, int) {
// On browsers, ScreenSizeInFullscreen returns the 'window' (global object) size, not 'screen' size for backward compatibility (#2145).
return window.Get("innerWidth").Int(), window.Get("innerHeight").Int()
}
@@ -0,0 +1,21 @@
// Copyright 2024 The Ebitengine Authors
//
// 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.
//go:build !js
package ui
func (u *UserInterface) ScreenSizeInFullscreen() (int, int) {
return u.Monitor().Size()
}
+118
View File
@@ -0,0 +1,118 @@
// Copyright 2022 The Ebiten Authors
//
// 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.
package ui
import (
"fmt"
"math"
"reflect"
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
"github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/shaderir"
)
type Shader struct {
shader *atlas.Shader
uniformNames []string
uniformTypes []shaderir.Type
uniformUint32Count int
}
func NewShader(ir *shaderir.Program) *Shader {
return &Shader{
shader: atlas.NewShader(ir),
uniformNames: ir.UniformNames[graphics.PreservedUniformVariablesCount:],
uniformTypes: ir.Uniforms[graphics.PreservedUniformVariablesCount:],
}
}
func (s *Shader) Deallocate() {
s.shader.Deallocate()
}
func (s *Shader) AppendUniforms(dst []uint32, uniforms map[string]any) []uint32 {
if s.uniformUint32Count == 0 {
for _, typ := range s.uniformTypes {
s.uniformUint32Count += typ.Uint32Count()
}
}
origLen := len(dst)
if cap(dst)-len(dst) >= s.uniformUint32Count {
dst = dst[:len(dst)+s.uniformUint32Count]
for i := origLen; i < len(dst); i++ {
dst[i] = 0
}
} else {
dst = append(dst, make([]uint32, s.uniformUint32Count)...)
}
idx := origLen
for i, name := range s.uniformNames {
typ := s.uniformTypes[i]
// Ignore if an unused name is specified (#2710).
if uv, ok := uniforms[name]; ok {
v := reflect.ValueOf(uv)
t := v.Type()
switch t.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if typ.Uint32Count() != 1 {
panic(fmt.Sprintf("ui: unexpected uniform value for %s (%s)", name, typ.String()))
}
dst[idx] = uint32(v.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
if typ.Uint32Count() != 1 {
panic(fmt.Sprintf("ui: unexpected uniform value for %s (%s)", name, typ.String()))
}
dst[idx] = uint32(v.Uint())
case reflect.Float32, reflect.Float64:
if typ.Uint32Count() != 1 {
panic(fmt.Sprintf("ui: unexpected uniform value for %s (%s)", name, typ.String()))
}
dst[idx] = math.Float32bits(float32(v.Float()))
case reflect.Slice, reflect.Array:
l := v.Len()
if typ.Uint32Count() != l {
panic(fmt.Sprintf("ui: unexpected uniform value for %s (%s)", name, typ.String()))
}
switch t.Elem().Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
for i := 0; i < l; i++ {
dst[idx+i] = uint32(v.Index(i).Int())
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
for i := 0; i < l; i++ {
dst[idx+i] = uint32(v.Index(i).Uint())
}
case reflect.Float32, reflect.Float64:
for i := 0; i < l; i++ {
dst[idx+i] = math.Float32bits(float32(v.Index(i).Float()))
}
default:
panic(fmt.Sprintf("ui: unexpected uniform value type: %s (%s)", name, v.Kind().String()))
}
default:
panic(fmt.Sprintf("ui: unexpected uniform value type: %s (%s)", name, v.Kind().String()))
}
}
idx += typ.Uint32Count()
}
return dst
}
+236
View File
@@ -0,0 +1,236 @@
// Copyright 2022 The Ebiten Authors
//
// 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.
package ui
import (
"errors"
"image"
"sync"
"sync/atomic"
_ "github.com/ebitengine/hideconsole"
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
"github.com/hajimehoshi/ebiten/v2/internal/mipmap"
"github.com/hajimehoshi/ebiten/v2/internal/thread"
)
// RegularTermination represents a regular termination.
// Run can return this error, and if this error is received,
// the game loop should be terminated as soon as possible.
var RegularTermination = errors.New("regular termination")
type FPSModeType int
const (
FPSModeVsyncOn FPSModeType = iota
FPSModeVsyncOffMaximum
FPSModeVsyncOffMinimum
)
type CursorMode int
const (
CursorModeVisible CursorMode = iota
CursorModeHidden
CursorModeCaptured
)
type CursorShape int
const (
CursorShapeDefault CursorShape = iota
CursorShapeText
CursorShapeCrosshair
CursorShapePointer
CursorShapeEWResize
CursorShapeNSResize
CursorShapeNESWResize
CursorShapeNWSEResize
CursorShapeMove
CursorShapeNotAllowed
)
type WindowResizingMode int
const (
WindowResizingModeDisabled WindowResizingMode = iota
WindowResizingModeOnlyFullscreenEnabled
WindowResizingModeEnabled
)
type UserInterface struct {
err error
errM sync.Mutex
isScreenClearedEveryFrame int32
graphicsLibrary int32
running int32
terminated int32
whiteImage *Image
mainThread thread.Thread
userInterfaceImpl
}
var (
theUI *UserInterface
)
func init() {
// newUserInterface() must be called in the main goroutine.
u, err := newUserInterface()
if err != nil {
panic(err)
}
theUI = u
}
func Get() *UserInterface {
return theUI
}
// newUserInterface must be called from the main thread.
func newUserInterface() (*UserInterface, error) {
u := &UserInterface{
isScreenClearedEveryFrame: 1,
graphicsLibrary: int32(GraphicsLibraryUnknown),
}
u.whiteImage = u.NewImage(3, 3, atlas.ImageTypeRegular)
pix := make([]byte, 4*u.whiteImage.width*u.whiteImage.height)
for i := range pix {
pix[i] = 0xff
}
// As a white image is used at Fill, use WritePixels instead.
u.whiteImage.WritePixels(pix, image.Rect(0, 0, u.whiteImage.width, u.whiteImage.height))
if err := u.init(); err != nil {
return nil, err
}
return u, nil
}
func (u *UserInterface) readPixels(mipmap *mipmap.Mipmap, pixels []byte, region image.Rectangle) error {
ok, err := mipmap.ReadPixels(u.graphicsDriver, pixels, region)
if err != nil {
return err
}
// ReadPixels failed since this was called in between two frames.
// Try this again at the next frame.
if !ok {
// If this function is called from the same sequence as a game's Update and Draw,
// this causes a dead lock.
// This never happens so far, but if handling inputs after EndFrame is implemented,
// this might be possible (#1704).
var err1 error
u.context.runInFrame(func() {
ok, err := mipmap.ReadPixels(u.graphicsDriver, pixels, region)
if err != nil {
err1 = err
return
}
if !ok {
// This never reaches since this function must be called in a frame.
panic("ui: ReadPixels unexpectedly failed")
}
})
return err1
}
return nil
}
func (u *UserInterface) dumpScreenshot(mipmap *mipmap.Mipmap, name string, blackbg bool) (string, error) {
return mipmap.DumpScreenshot(u.graphicsDriver, name, blackbg)
}
func (u *UserInterface) dumpImages(dir string) (string, error) {
return atlas.DumpImages(u.graphicsDriver, dir)
}
type RunOptions struct {
GraphicsLibrary GraphicsLibrary
InitUnfocused bool
ScreenTransparent bool
SkipTaskbar bool
SingleThread bool
X11ClassName string
X11InstanceName string
}
// InitialWindowPosition returns the position for centering the given second width/height pair within the first width/height pair.
func InitialWindowPosition(mw, mh, ww, wh int) (x, y int) {
return (mw - ww) / 2, (mh - wh) / 3
}
func (u *UserInterface) error() error {
u.errM.Lock()
defer u.errM.Unlock()
return u.err
}
func (u *UserInterface) setError(err error) {
u.errM.Lock()
defer u.errM.Unlock()
if u.err == nil {
u.err = err
}
}
func (u *UserInterface) IsScreenClearedEveryFrame() bool {
return atomic.LoadInt32(&u.isScreenClearedEveryFrame) != 0
}
func (u *UserInterface) SetScreenClearedEveryFrame(cleared bool) {
v := int32(0)
if cleared {
v = 1
}
atomic.StoreInt32(&u.isScreenClearedEveryFrame, v)
}
func (u *UserInterface) setGraphicsLibrary(library GraphicsLibrary) {
atomic.StoreInt32(&u.graphicsLibrary, int32(library))
}
func (u *UserInterface) GraphicsLibrary() GraphicsLibrary {
return GraphicsLibrary(atomic.LoadInt32(&u.graphicsLibrary))
}
func (u *UserInterface) isRunning() bool {
return atomic.LoadInt32(&u.running) != 0 && !u.isTerminated()
}
func (u *UserInterface) setRunning(running bool) {
if running {
atomic.StoreInt32(&u.running, 1)
} else {
atomic.StoreInt32(&u.running, 0)
}
}
func (u *UserInterface) isTerminated() bool {
return atomic.LoadInt32(&u.terminated) != 0
}
func (u *UserInterface) setTerminated() {
atomic.StoreInt32(&u.terminated, 1)
}
+135
View File
@@ -0,0 +1,135 @@
// Copyright 2022 The Ebiten Authors
//
// 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.
package ui
/*
#include <jni.h>
#include <stdlib.h>
// Basically same as:
//
// WindowService windowService = context.getSystemService(Context.WINDOW_SERVICE);
// Display display = windowManager.getDefaultDisplay();
// DisplayMetrics displayMetrics = new DisplayMetrics();
// display.getRealMetrics(displayMetrics);
// this.deviceScale = displayMetrics.density;
//
static float deviceScale(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) {
JavaVM* vm = (JavaVM*)java_vm;
JNIEnv* env = (JNIEnv*)jni_env;
jobject context = (jobject)ctx;
const char* kWindowService = "window";
const jclass android_content_Context =
(*env)->FindClass(env, "android/content/Context");
const jclass android_view_WindowManager =
(*env)->FindClass(env, "android/view/WindowManager");
const jclass android_view_Display =
(*env)->FindClass(env, "android/view/Display");
const jclass android_util_DisplayMetrics =
(*env)->FindClass(env, "android/util/DisplayMetrics");
const jobject android_context_Context_WINDOW_SERVICE =
(*env)->GetStaticObjectField(
env, android_content_Context,
(*env)->GetStaticFieldID(env, android_content_Context, "WINDOW_SERVICE", "Ljava/lang/String;"));
const jobject windowManager =
(*env)->CallObjectMethod(
env, context,
(*env)->GetMethodID(env, android_content_Context, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;"),
android_context_Context_WINDOW_SERVICE);
const jobject display =
(*env)->CallObjectMethod(
env, windowManager,
(*env)->GetMethodID(env, android_view_WindowManager, "getDefaultDisplay", "()Landroid/view/Display;"));
const jobject displayMetrics =
(*env)->NewObject(
env, android_util_DisplayMetrics,
(*env)->GetMethodID(env, android_util_DisplayMetrics, "<init>", "()V"));
(*env)->CallVoidMethod(
env, display,
(*env)->GetMethodID(env, android_view_Display, "getRealMetrics", "(Landroid/util/DisplayMetrics;)V"),
displayMetrics);
const float density =
(*env)->GetFloatField(
env, displayMetrics,
(*env)->GetFieldID(env, android_util_DisplayMetrics, "density", "F"));
(*env)->DeleteLocalRef(env, android_content_Context);
(*env)->DeleteLocalRef(env, android_view_WindowManager);
(*env)->DeleteLocalRef(env, android_view_Display);
(*env)->DeleteLocalRef(env, android_util_DisplayMetrics);
(*env)->DeleteLocalRef(env, android_context_Context_WINDOW_SERVICE);
(*env)->DeleteLocalRef(env, windowManager);
(*env)->DeleteLocalRef(env, display);
(*env)->DeleteLocalRef(env, displayMetrics);
return density;
}
*/
import "C"
import (
"errors"
"fmt"
"github.com/ebitengine/gomobile/app"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/opengl"
)
type graphicsDriverCreatorImpl struct {
}
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
graphics, err := g.newOpenGL()
return graphics, GraphicsLibraryOpenGL, err
}
func (g *graphicsDriverCreatorImpl) newOpenGL() (graphicsdriver.Graphics, error) {
return opengl.NewGraphics()
}
func (*graphicsDriverCreatorImpl) newDirectX() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: DirectX is not supported in this environment")
}
func (*graphicsDriverCreatorImpl) newMetal() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: Metal is not supported in this environment")
}
func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: PlayStation 5 is not supported in this environment")
}
func deviceScaleFactorImpl() float64 {
var s float64
if err := app.RunOnJVM(func(vm, env, ctx uintptr) error {
// TODO: This might be crash when this is called from init(). How can we detect this?
s = float64(C.deviceScale(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx)))
return nil
}); err != nil {
panic(fmt.Sprintf("devicescale: error %v", err))
}
return s
}
func dipToNativePixels(x float64, scale float64) float64 {
return x * scale
}
+436
View File
@@ -0,0 +1,436 @@
// Copyright 2016 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.
//go:build !ios
package ui
import (
"errors"
"fmt"
"reflect"
"unsafe"
"github.com/ebitengine/purego/objc"
"github.com/hajimehoshi/ebiten/v2/internal/cocoa"
"github.com/hajimehoshi/ebiten/v2/internal/glfw"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/metal"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/opengl"
)
var class_EbitengineWindowDelegate objc.Class
func (u *UserInterface) initializePlatform() error {
pushResizableState := func(id, win objc.ID) {
window := cocoa.NSWindow{ID: win}
id.Send(sel_setOrigResizable, window.StyleMask()&cocoa.NSWindowStyleMaskResizable != 0)
if !objc.Send[bool](id, sel_origResizable) {
window.SetStyleMask(window.StyleMask() | cocoa.NSWindowStyleMaskResizable)
}
}
popResizableState := func(id, win objc.ID) {
if !objc.Send[bool](id, sel_origResizable) {
window := cocoa.NSWindow{ID: win}
window.SetStyleMask(window.StyleMask() & ^uint(cocoa.NSWindowStyleMaskResizable))
}
id.Send(sel_setOrigResizable, false)
}
d, err := objc.RegisterClass(
"EbitengineWindowDelegate",
objc.GetClass("NSObject"),
[]*objc.Protocol{objc.GetProtocol("NSWindowDelegate")},
[]objc.FieldDef{
{
Name: "origDelegate",
Type: reflect.TypeOf(objc.ID(0)),
Attribute: objc.ReadWrite,
},
{
Name: "origResizable",
Type: reflect.TypeOf(true),
Attribute: objc.ReadWrite,
},
},
[]objc.MethodDef{
{
Cmd: sel_initWithOrigDelegate,
Fn: func(id objc.ID, cmd objc.SEL, origDelegate objc.ID) objc.ID {
self := id.SendSuper(sel_init)
if self != 0 {
id.Send(sel_setOrigDelegate, origDelegate)
}
return self
},
},
// The method set of origDelegate must sync with GLFWWindowDelegate's implementation.
// See cocoa_window.m in GLFW.
{
Cmd: sel_windowShouldClose,
Fn: func(id objc.ID, cmd objc.SEL, notification objc.ID) bool {
return id.Send(sel_origDelegate).Send(cmd, notification) != 0
},
},
{
Cmd: sel_windowDidResize,
Fn: func(id objc.ID, cmd objc.SEL, notification objc.ID) {
id.Send(sel_origDelegate).Send(cmd, notification)
},
},
{
Cmd: sel_windowDidMove,
Fn: func(id objc.ID, cmd objc.SEL, notification objc.ID) {
id.Send(sel_origDelegate).Send(cmd, notification)
},
},
{
Cmd: sel_windowDidMiniaturize,
Fn: func(id objc.ID, cmd objc.SEL, notification objc.ID) {
id.Send(sel_origDelegate).Send(cmd, notification)
},
},
{
Cmd: sel_windowDidBecomeKey,
Fn: func(id objc.ID, cmd objc.SEL, notification objc.ID) {
id.Send(sel_origDelegate).Send(cmd, notification)
},
},
{
Cmd: sel_windowDidResignKey,
Fn: func(id objc.ID, cmd objc.SEL, notification objc.ID) {
id.Send(sel_origDelegate).Send(cmd, notification)
},
},
{
Cmd: sel_windowDidChangeOcclusionState,
Fn: func(id objc.ID, cmd objc.SEL, notification objc.ID) {
id.Send(sel_origDelegate).Send(cmd, notification)
},
},
{
Cmd: sel_windowWillEnterFullScreen,
Fn: func(id objc.ID, cmd objc.SEL, notification objc.ID) {
if err := u.setOrigWindowPosWithCurrentPos(); err != nil {
u.setError(err)
return
}
pushResizableState(id, cocoa.NSNotification{ID: notification}.Object())
},
},
{
Cmd: sel_windowDidEnterFullScreen,
Fn: func(id objc.ID, cmd objc.SEL, notification objc.ID) {
popResizableState(id, cocoa.NSNotification{ID: notification}.Object())
},
},
{
Cmd: sel_windowWillExitFullScreen,
Fn: func(id objc.ID, cmd objc.SEL, notification objc.ID) {
pushResizableState(id, cocoa.NSNotification{ID: notification}.Object())
// Even a window has a size limitation, a window can be fullscreen by calling SetFullscreen(true).
// In this case, the window size limitation is disabled temporarily.
// When exiting from fullscreen, reset the window size limitation.
if err := u.updateWindowSizeLimits(); err != nil {
u.setError(err)
return
}
},
},
{
Cmd: sel_windowDidExitFullScreen,
Fn: func(id objc.ID, cmd objc.SEL, notification objc.ID) {
popResizableState(id, cocoa.NSNotification{ID: notification}.Object())
// Do not call setFrame here (#2295). setFrame here causes unexpected results.
},
},
},
)
if err != nil {
return err
}
class_EbitengineWindowDelegate = d
return nil
}
type graphicsDriverCreatorImpl struct {
transparent bool
}
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
m, err1 := g.newMetal()
if err1 == nil {
return m, GraphicsLibraryMetal, nil
}
o, err2 := g.newOpenGL()
if err2 == nil {
return o, GraphicsLibraryOpenGL, nil
}
return nil, GraphicsLibraryUnknown, fmt.Errorf("ui: failed to choose graphics drivers: Metal: %v, OpenGL: %v", err1, err2)
}
func (*graphicsDriverCreatorImpl) newOpenGL() (graphicsdriver.Graphics, error) {
return opengl.NewGraphics()
}
func (*graphicsDriverCreatorImpl) newDirectX() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: DirectX is not supported in this environment")
}
func (*graphicsDriverCreatorImpl) newMetal() (graphicsdriver.Graphics, error) {
return metal.NewGraphics()
}
func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: PlayStation 5 is not supported in this environment")
}
// glfwMonitorSizeInGLFWPixels must be called from the main thread.
func glfwMonitorSizeInGLFWPixels(m *glfw.Monitor) (int, int, error) {
vm, err := m.GetVideoMode()
if err != nil {
return 0, 0, err
}
return vm.Width, vm.Height, nil
}
func dipFromGLFWPixel(x float64, scale float64) float64 {
// NOTE: On macOS, GLFW exposes the device independent coordinate system.
// Thus, the conversion functions are unnecessary,
// however we still need the deviceScaleFactor internally
// so we can create and maintain a HiDPI frame buffer.
return x
}
func dipToGLFWPixel(x float64, scale float64) float64 {
return x
}
func (u *UserInterface) adjustWindowPosition(x, y int, monitor *Monitor) (int, int) {
return x, y
}
var (
class_NSCursor = objc.GetClass("NSCursor")
class_NSEvent = objc.GetClass("NSEvent")
)
var (
sel_alloc = objc.RegisterName("alloc")
sel_collectionBehavior = objc.RegisterName("collectionBehavior")
sel_delegate = objc.RegisterName("delegate")
sel_init = objc.RegisterName("init")
sel_initWithOrigDelegate = objc.RegisterName("initWithOrigDelegate:")
sel_mouseLocation = objc.RegisterName("mouseLocation")
sel_origDelegate = objc.RegisterName("origDelegate")
sel_origResizable = objc.RegisterName("isOrigResizable")
sel_setCollectionBehavior = objc.RegisterName("setCollectionBehavior:")
sel_setDelegate = objc.RegisterName("setDelegate:")
sel_setOrigDelegate = objc.RegisterName("setOrigDelegate:")
sel_setOrigResizable = objc.RegisterName("setOrigResizable:")
sel_toggleFullScreen = objc.RegisterName("toggleFullScreen:")
sel_windowDidBecomeKey = objc.RegisterName("windowDidBecomeKey:")
sel_windowDidDeminiaturize = objc.RegisterName("windowDidDeminiaturize:")
sel_windowDidEnterFullScreen = objc.RegisterName("windowDidEnterFullScreen:")
sel_windowDidExitFullScreen = objc.RegisterName("windowDidExitFullScreen:")
sel_windowDidMiniaturize = objc.RegisterName("windowDidMiniaturize:")
sel_windowDidMove = objc.RegisterName("windowDidMove:")
sel_windowDidResignKey = objc.RegisterName("windowDidResignKey:")
sel_windowDidResize = objc.RegisterName("windowDidResize:")
sel_windowDidChangeOcclusionState = objc.RegisterName("windowDidChangeOcclusionState:")
sel_windowShouldClose = objc.RegisterName("windowShouldClose:")
sel_windowWillEnterFullScreen = objc.RegisterName("windowWillEnterFullScreen:")
sel_windowWillExitFullScreen = objc.RegisterName("windowWillExitFullScreen:")
)
func currentMouseLocation() (x, y int) {
sig := cocoa.NSMethodSignature_signatureWithObjCTypes("{NSPoint=dd}@:")
inv := cocoa.NSInvocation_invocationWithMethodSignature(sig)
inv.SetTarget(objc.ID(class_NSEvent))
inv.SetSelector(sel_mouseLocation)
inv.Invoke()
var point cocoa.NSPoint
inv.GetReturnValue(unsafe.Pointer(&point))
x, y = int(point.X), int(point.Y)
// On macOS, the Y axis is upward. Adjust the Y position (#807, #2794).
y = -y
m := theMonitors.primaryMonitor()
y += m.videoMode.Height
return x, y
}
func initialMonitorByOS() (*Monitor, error) {
x, y := currentMouseLocation()
// Find the monitor including the cursor.
return theMonitors.monitorFromPosition(x, y), nil
}
func monitorFromWindowByOS(w *glfw.Window) (*Monitor, error) {
cocoaWindow, err := w.GetCocoaWindow()
if err != nil {
return nil, err
}
window := cocoa.NSWindow{ID: objc.ID(cocoaWindow)}
pool := cocoa.NSAutoreleasePool_new()
screen := cocoa.NSScreen_mainScreen()
if window.ID != 0 && window.IsVisible() {
// When the window is visible, the window is already initialized.
// [NSScreen mainScreen] sometimes tells a lie when the window is put across monitors (#703).
screen = window.Screen()
}
screenDictionary := screen.DeviceDescription()
screenID := cocoa.NSNumber{ID: screenDictionary.ObjectForKey(cocoa.NSString_alloc().InitWithUTF8String("NSScreenNumber").ID)}
aID := uintptr(screenID.UnsignedIntValue()) // CGDirectDisplayID
pool.Release()
for _, m := range theMonitors.append(nil) {
cocoaMonitor, err := m.m.GetCocoaMonitor()
if err != nil {
return nil, err
}
if cocoaMonitor == aID {
return m, nil
}
}
return nil, nil
}
func (u *UserInterface) nativeWindow() (uintptr, error) {
return u.window.GetCocoaWindow()
}
func (u *UserInterface) isNativeFullscreen() (bool, error) {
w, err := u.window.GetCocoaWindow()
if err != nil {
return false, err
}
return cocoa.NSWindow{ID: objc.ID(w)}.StyleMask()&cocoa.NSWindowStyleMaskFullScreen != 0, nil
}
func (u *UserInterface) isNativeFullscreenAvailable() bool {
// TODO: If the window is transparent, we should use GLFW's windowed fullscreen (#1822, #1857).
// However, if the user clicks the green button, should this window be in native fullscreen mode?
return true
}
func (u *UserInterface) setNativeFullscreen(fullscreen bool) error {
// Toggling fullscreen might ignore events like keyUp. Ensure that events are fired.
if err := glfw.WaitEventsTimeout(0.1); err != nil {
return err
}
w, err := u.window.GetCocoaWindow()
if err != nil {
return err
}
window := cocoa.NSWindow{ID: objc.ID(w)}
if window.StyleMask()&cocoa.NSWindowStyleMaskFullScreen != 0 == fullscreen {
return nil
}
// Even though EbitengineWindowDelegate is used, this hack is still required.
// toggleFullscreen doesn't work when the window is not resizable.
origCollectionBehavior := window.Send(sel_collectionBehavior)
origFullScreen := origCollectionBehavior&cocoa.NSWindowCollectionBehaviorFullScreenPrimary != 0
if !origFullScreen {
collectionBehavior := origCollectionBehavior
collectionBehavior |= cocoa.NSWindowCollectionBehaviorFullScreenPrimary
collectionBehavior &^= cocoa.NSWindowCollectionBehaviorFullScreenNone
window.Send(sel_setCollectionBehavior, cocoa.NSUInteger(collectionBehavior))
}
window.Send(sel_toggleFullScreen, 0)
if !origFullScreen {
window.Send(sel_setCollectionBehavior, cocoa.NSUInteger(cocoa.NSUInteger(origCollectionBehavior)))
}
return nil
}
func (u *UserInterface) adjustViewSizeAfterFullscreen() error {
if u.GraphicsLibrary() == GraphicsLibraryOpenGL {
return nil
}
w, err := u.window.GetCocoaWindow()
if err != nil {
return err
}
window := cocoa.NSWindow{ID: objc.ID(w)}
if window.StyleMask()&cocoa.NSWindowStyleMaskFullScreen == 0 {
return nil
}
// Reduce the view height (#1745).
// https://stackoverflow.com/questions/27758027/sprite-kit-serious-fps-issue-in-full-screen-mode-on-os-x
windowSize := window.Frame().Size
view := window.ContentView()
viewSize := view.Frame().Size
if windowSize.Width != viewSize.Width || windowSize.Height != viewSize.Height {
return nil
}
viewSize.Width--
view.SetFrameSize(viewSize)
// NSColor.blackColor (0, 0, 0, 1) didn't work.
// Use the transparent color instead.
window.SetBackgroundColor(cocoa.NSColor_colorWithSRGBRedGreenBlueAlpha(0, 0, 0, 0))
return nil
}
func (u *UserInterface) isFullscreenAllowedFromUI(mode WindowResizingMode) bool {
if u.maxWindowWidthInDIP != glfw.DontCare || u.maxWindowHeightInDIP != glfw.DontCare {
return false
}
if mode == WindowResizingModeOnlyFullscreenEnabled {
return true
}
if mode == WindowResizingModeEnabled {
return true
}
return false
}
func (u *UserInterface) setWindowResizingModeForOS(mode WindowResizingMode) error {
var collectionBehavior uint
if u.isFullscreenAllowedFromUI(mode) {
collectionBehavior |= cocoa.NSWindowCollectionBehaviorManaged
collectionBehavior |= cocoa.NSWindowCollectionBehaviorFullScreenPrimary
} else {
collectionBehavior |= cocoa.NSWindowCollectionBehaviorFullScreenNone
}
w, err := u.window.GetCocoaWindow()
if err != nil {
return err
}
objc.ID(w).Send(sel_setCollectionBehavior, collectionBehavior)
return nil
}
func initializeWindowAfterCreation(w *glfw.Window) error {
// TODO: Register NSWindowWillEnterFullScreenNotification and so on.
// Enable resizing temporary before making the window fullscreen.
cocoaWindow, err := w.GetCocoaWindow()
if err != nil {
return err
}
nswindow := objc.ID(cocoaWindow)
delegate := objc.ID(class_EbitengineWindowDelegate).Send(sel_alloc).Send(sel_initWithOrigDelegate, nswindow.Send(sel_delegate))
nswindow.Send(sel_setDelegate, delegate)
return nil
}
func (u *UserInterface) skipTaskbar() error {
return nil
}
File diff suppressed because it is too large Load Diff
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2022 The Ebiten Authors
//
// 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.
package ui
// #cgo CFLAGS: -x objective-c
// #cgo LDFLAGS: -framework Foundation -framework UIKit
//
// #import <UIKit/UIKit.h>
//
// static double devicePixelRatio() {
// return [[UIScreen mainScreen] nativeScale];
// }
import "C"
import (
"errors"
"fmt"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/metal"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/opengl"
)
type graphicsDriverCreatorImpl struct {
}
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
m, err1 := g.newMetal()
if err1 == nil {
return m, GraphicsLibraryMetal, nil
}
o, err2 := g.newOpenGL()
if err2 == nil {
return o, GraphicsLibraryMetal, nil
}
return nil, GraphicsLibraryUnknown, fmt.Errorf("ui: failed to choose graphics drivers: Metal: %v, OpenGL: %v", err1, err2)
}
func (g *graphicsDriverCreatorImpl) newOpenGL() (graphicsdriver.Graphics, error) {
return opengl.NewGraphics()
}
func (*graphicsDriverCreatorImpl) newDirectX() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: DirectX is not supported in this environment")
}
func (g *graphicsDriverCreatorImpl) newMetal() (graphicsdriver.Graphics, error) {
return metal.NewGraphics()
}
func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: PlayStation 5 is not supported in this environment")
}
func (u *UserInterface) SetUIView(uiview uintptr) error {
select {
case err := <-u.errCh:
return err
case <-u.graphicsLibraryInitCh:
}
// This function should be called only when the graphics library is Metal.
if g, ok := u.graphicsDriver.(interface{ SetUIView(uintptr) }); ok {
g.SetUIView(uiview)
}
return nil
}
func (u *UserInterface) IsGL() (bool, error) {
select {
case err := <-u.errCh:
return false, err
case <-u.graphicsLibraryInitCh:
}
return u.GraphicsLibrary() == GraphicsLibraryOpenGL, nil
}
func deviceScaleFactorImpl() float64 {
// TODO: Can this be called from non-main threads?
return float64(C.devicePixelRatio())
}
func dipToNativePixels(x float64, scale float64) float64 {
return x
}
+828
View File
@@ -0,0 +1,828 @@
// 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.
package ui
import (
"errors"
"math"
"sync"
"syscall/js"
"time"
"github.com/hajimehoshi/ebiten/v2/internal/file"
"github.com/hajimehoshi/ebiten/v2/internal/gamepad"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/opengl"
"github.com/hajimehoshi/ebiten/v2/internal/hook"
)
type graphicsDriverCreatorImpl struct {
canvas js.Value
}
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
graphics, err := g.newOpenGL()
return graphics, GraphicsLibraryOpenGL, err
}
func (g *graphicsDriverCreatorImpl) newOpenGL() (graphicsdriver.Graphics, error) {
return opengl.NewGraphics(g.canvas)
}
func (*graphicsDriverCreatorImpl) newDirectX() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: DirectX is not supported in this environment")
}
func (*graphicsDriverCreatorImpl) newMetal() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: Metal is not supported in this environment")
}
func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: PlayStation 5 is not supported in this environment")
}
var (
stringNone = js.ValueOf("none")
stringTransparent = js.ValueOf("transparent")
)
func driverCursorShapeToCSSCursor(cursor CursorShape) string {
switch cursor {
case CursorShapeDefault:
return "default"
case CursorShapeText:
return "text"
case CursorShapeCrosshair:
return "crosshair"
case CursorShapePointer:
return "pointer"
case CursorShapeEWResize:
return "ew-resize"
case CursorShapeNSResize:
return "ns-resize"
case CursorShapeNESWResize:
return "nesw-resize"
case CursorShapeNWSEResize:
return "nwse-resize"
case CursorShapeMove:
return "move"
case CursorShapeNotAllowed:
return "not-allowed"
}
return "auto"
}
type userInterfaceImpl struct {
graphicsDriver graphicsdriver.Graphics
runnableOnUnfocused bool
fpsMode FPSModeType
renderingScheduled bool
cursorMode CursorMode
cursorPrevMode CursorMode
captureCursorLater bool
cursorShape CursorShape
onceUpdateCalled bool
lastCaptureExitTime time.Time
context *context
inputState InputState
keyDurationsByKeyProperty map[Key]int
cursorXInClient float64
cursorYInClient float64
origCursorXInClient float64
origCursorYInClient float64
touchesInClient []touchInClient
savedCursorX float64
savedCursorY float64
savedOutsideWidth float64
savedOutsideHeight float64
outsideSizeUnchangedCount int
keyboardLayoutMap js.Value
m sync.Mutex
dropFileM sync.Mutex
}
var (
window = js.Global().Get("window")
document = js.Global().Get("document")
screen = js.Global().Get("screen")
canvas js.Value
requestAnimationFrame = js.Global().Get("requestAnimationFrame")
setTimeout = js.Global().Get("setTimeout")
)
var (
documentHasFocus = document.Get("hasFocus").Call("bind", document)
documentHidden = js.Global().Get("Object").Call("getOwnPropertyDescriptor", js.Global().Get("Document").Get("prototype"), "hidden").Get("get").Call("bind", document)
)
func (u *UserInterface) SetFullscreen(fullscreen bool) {
if !canvas.Truthy() {
return
}
if !document.Truthy() {
return
}
if fullscreen == u.IsFullscreen() {
return
}
if u.cursorMode == CursorModeCaptured {
u.saveCursorPosition()
}
if fullscreen {
f := canvas.Get("requestFullscreen")
if !f.Truthy() {
f = canvas.Get("webkitRequestFullscreen")
}
f.Call("bind", canvas).Invoke()
return
}
f := document.Get("exitFullscreen")
if !f.Truthy() {
f = document.Get("webkitExitFullscreen")
}
f.Call("bind", document).Invoke()
}
func (u *UserInterface) IsFullscreen() bool {
if !document.Truthy() {
return false
}
if !document.Get("fullscreenElement").Truthy() && !document.Get("webkitFullscreenElement").Truthy() {
return false
}
return true
}
func (u *UserInterface) IsFocused() bool {
return u.isFocused()
}
func (u *UserInterface) SetRunnableOnUnfocused(runnableOnUnfocused bool) {
u.runnableOnUnfocused = runnableOnUnfocused
}
func (u *UserInterface) IsRunnableOnUnfocused() bool {
return u.runnableOnUnfocused
}
func (u *UserInterface) FPSMode() FPSModeType {
return u.fpsMode
}
func (u *UserInterface) SetFPSMode(mode FPSModeType) {
u.fpsMode = mode
}
func (u *UserInterface) ScheduleFrame() {
u.renderingScheduled = true
}
func (u *UserInterface) CursorMode() CursorMode {
if !canvas.Truthy() {
return CursorModeHidden
}
return u.cursorMode
}
func (u *UserInterface) SetCursorMode(mode CursorMode) {
if mode == CursorModeCaptured && !u.canCaptureCursor() {
u.captureCursorLater = true
return
}
u.setCursorMode(mode)
}
func (u *UserInterface) setCursorMode(mode CursorMode) {
u.captureCursorLater = false
if !canvas.Truthy() {
return
}
if u.cursorMode == mode {
return
}
// Remember the previous cursor mode in the case when the pointer lock exits by pressing ESC.
u.cursorPrevMode = u.cursorMode
if u.cursorMode == CursorModeCaptured {
document.Call("exitPointerLock")
u.lastCaptureExitTime = time.Now()
}
u.cursorMode = mode
switch mode {
case CursorModeVisible:
canvas.Get("style").Set("cursor", driverCursorShapeToCSSCursor(u.cursorShape))
case CursorModeHidden:
canvas.Get("style").Set("cursor", stringNone)
case CursorModeCaptured:
canvas.Call("requestPointerLock")
}
}
func (u *UserInterface) recoverCursorMode() {
if u.cursorPrevMode == CursorModeCaptured {
panic("ui: cursorPrevMode must not be CursorModeCaptured at recoverCursorMode")
}
u.SetCursorMode(u.cursorPrevMode)
}
func (u *UserInterface) CursorShape() CursorShape {
if !canvas.Truthy() {
return CursorShapeDefault
}
return u.cursorShape
}
func (u *UserInterface) SetCursorShape(shape CursorShape) {
if !canvas.Truthy() {
return
}
if u.cursorShape == shape {
return
}
u.cursorShape = shape
if u.cursorMode == CursorModeVisible {
canvas.Get("style").Set("cursor", driverCursorShapeToCSSCursor(u.cursorShape))
}
}
func (u *UserInterface) outsideSize() (float64, float64) {
if document.Truthy() {
body := document.Get("body")
bw := body.Get("clientWidth").Float()
bh := body.Get("clientHeight").Float()
return bw, bh
}
// Node.js
return 640, 480
}
func (u *UserInterface) suspended() bool {
if u.runnableOnUnfocused {
return false
}
return !u.isFocused()
}
func (u *UserInterface) isFocused() bool {
if !documentHasFocus.Invoke().Bool() {
return false
}
if documentHidden.Invoke().Bool() {
return false
}
return true
}
// 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.
func (u *UserInterface) canCaptureCursor() bool {
// 1.5 [sec] seems enough in the real world.
return time.Now().Sub(u.lastCaptureExitTime) >= 1500*time.Millisecond
}
func (u *UserInterface) update() error {
if u.captureCursorLater && u.canCaptureCursor() {
u.setCursorMode(CursorModeCaptured)
}
if u.suspended() {
return hook.SuspendAudio()
}
if err := hook.ResumeAudio(); err != nil {
return err
}
return u.updateImpl(false)
}
func (u *UserInterface) updateImpl(force bool) error {
// Guard updateImpl as this function cannot be invoked until this finishes (#2339).
u.m.Lock()
defer u.m.Unlock()
// context can be nil when an event is fired but the loop doesn't start yet (#1928).
if u.context == nil {
return nil
}
if err := gamepad.Update(); err != nil {
return err
}
// TODO: If DeviceScaleFactor changes, call updateScreenSize.
// Now there is not a good way to detect the change.
// See also https://crbug.com/123694.
w, h := u.outsideSize()
if force {
if err := u.context.forceUpdateFrame(u.graphicsDriver, w, h, theMonitor.DeviceScaleFactor(), u); err != nil {
return err
}
} else {
if err := u.context.updateFrame(u.graphicsDriver, w, h, theMonitor.DeviceScaleFactor(), u); err != nil {
return err
}
}
return nil
}
func (u *UserInterface) needsUpdate() bool {
if u.fpsMode != FPSModeVsyncOffMinimum {
return true
}
if !u.onceUpdateCalled {
return true
}
if u.renderingScheduled {
return true
}
// TODO: Watch the gamepad state?
return false
}
func (u *UserInterface) loopGame() error {
// Initialize the screen size first (#3033).
// If ebiten.SetRunnableOnUnfocused(false) and the canvas is not focused,
// suspended() returns true and the update routine cannot start.
u.updateScreenSize()
errCh := make(chan error, 1)
reqStopAudioCh := make(chan struct{})
resStopAudioCh := make(chan struct{})
var cf js.Func
f := func() {
if err := u.error(); err != nil {
errCh <- err
return
}
if u.needsUpdate() {
defer func() {
u.onceUpdateCalled = true
}()
u.renderingScheduled = false
if err := u.update(); err != nil {
close(reqStopAudioCh)
<-resStopAudioCh
errCh <- err
return
}
}
switch u.fpsMode {
case FPSModeVsyncOn:
requestAnimationFrame.Invoke(cf)
case FPSModeVsyncOffMaximum:
setTimeout.Invoke(cf, 0)
case FPSModeVsyncOffMinimum:
requestAnimationFrame.Invoke(cf)
}
}
// TODO: Should cf be released after the game ends?
cf = js.FuncOf(func(this js.Value, args []js.Value) any {
// f can be blocked but callbacks must not be blocked. Create a goroutine (#1161).
go f()
return nil
})
// Call f asyncly since ch is used in f.
go f()
// Run another loop to watch suspended() as the above update function is never called when the tab is hidden.
// To check the document's visibility, 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() {
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() {
if err := hook.SuspendAudio(); err != nil {
errCh <- err
return
}
} else {
if err := hook.ResumeAudio(); err != nil {
errCh <- err
return
}
}
case <-reqStopAudioCh:
return
}
}
}()
return <-errCh
}
func (u *UserInterface) init() error {
u.userInterfaceImpl = userInterfaceImpl{
runnableOnUnfocused: true,
savedCursorX: math.NaN(),
savedCursorY: math.NaN(),
}
// document is undefined on node.js
if !document.Truthy() {
return nil
}
if !document.Get("body").Truthy() {
ch := make(chan struct{})
window.Call("addEventListener", "load", js.FuncOf(func(this js.Value, args []js.Value) any {
close(ch)
return nil
}))
<-ch
}
u.setWindowEventHandlers(window)
// Adjust the initial scale to 1.
// https://developer.mozilla.org/en/docs/Mozilla/Mobile/Viewport_meta_tag
meta := document.Call("createElement", "meta")
meta.Set("name", "viewport")
meta.Set("content", "width=device-width, initial-scale=1")
document.Get("head").Call("appendChild", meta)
canvas = document.Call("createElement", "canvas")
canvas.Set("width", 16)
canvas.Set("height", 16)
document.Get("body").Call("appendChild", canvas)
htmlStyle := document.Get("documentElement").Get("style")
htmlStyle.Set("height", "100%")
htmlStyle.Set("margin", "0")
htmlStyle.Set("padding", "0")
bodyStyle := document.Get("body").Get("style")
bodyStyle.Set("backgroundColor", "#000")
bodyStyle.Set("height", "100%")
bodyStyle.Set("margin", "0")
bodyStyle.Set("padding", "0")
canvasStyle := canvas.Get("style")
canvasStyle.Set("width", "100%")
canvasStyle.Set("height", "100%")
canvasStyle.Set("margin", "0")
canvasStyle.Set("padding", "0")
// Make the canvas focusable.
canvas.Call("setAttribute", "tabindex", 1)
canvas.Get("style").Set("outline", "none")
u.setCanvasEventHandlers(canvas)
// Pointer Lock
document.Call("addEventListener", "pointerlockchange", js.FuncOf(func(this js.Value, args []js.Value) any {
if document.Get("pointerLockElement").Truthy() {
return nil
}
// Recover the state correctly when the pointer lock exits.
// A user can exit the pointer lock by pressing ESC. In this case, sync the cursor mode state.
if u.cursorMode == CursorModeCaptured {
u.recoverCursorMode()
}
u.recoverCursorPosition()
return nil
}))
document.Call("addEventListener", "pointerlockerror", js.FuncOf(func(this js.Value, args []js.Value) any {
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.")
return nil
}))
document.Call("addEventListener", "fullscreenerror", js.FuncOf(func(this js.Value, args []js.Value) any {
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.")
return nil
}))
document.Call("addEventListener", "webkitfullscreenerror", js.FuncOf(func(this js.Value, args []js.Value) any {
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.")
return nil
}))
return nil
}
func (u *UserInterface) setWindowEventHandlers(v js.Value) {
v.Call("addEventListener", "resize", js.FuncOf(func(this js.Value, args []js.Value) any {
u.updateScreenSize()
// updateImpl can block. Use goroutine.
// See https://pkg.go.dev/syscall/js#FuncOf.
go func() {
if err := u.updateImpl(true); err != nil {
u.setError(err)
return
}
}()
return nil
}))
}
func (u *UserInterface) setCanvasEventHandlers(v js.Value) {
// Keyboard
v.Call("addEventListener", "keydown", js.FuncOf(func(this js.Value, args []js.Value) any {
// Focus the canvas explicitly to activate tha game (#961).
v.Call("focus")
e := args[0]
e.Call("preventDefault")
if err := u.updateInputFromEvent(e); err != nil {
u.setError(err)
return nil
}
return nil
}))
v.Call("addEventListener", "keyup", js.FuncOf(func(this js.Value, args []js.Value) any {
e := args[0]
e.Call("preventDefault")
if err := u.updateInputFromEvent(e); err != nil {
u.setError(err)
return nil
}
return nil
}))
// Mouse
v.Call("addEventListener", "mousedown", js.FuncOf(func(this js.Value, args []js.Value) any {
// Focus the canvas explicitly to activate tha game (#961).
v.Call("focus")
e := args[0]
e.Call("preventDefault")
if err := u.updateInputFromEvent(e); err != nil {
u.setError(err)
return nil
}
return nil
}))
v.Call("addEventListener", "mouseup", js.FuncOf(func(this js.Value, args []js.Value) any {
e := args[0]
e.Call("preventDefault")
if err := u.updateInputFromEvent(e); err != nil {
u.setError(err)
return nil
}
return nil
}))
v.Call("addEventListener", "mousemove", js.FuncOf(func(this js.Value, args []js.Value) any {
e := args[0]
e.Call("preventDefault")
if err := u.updateInputFromEvent(e); err != nil {
u.setError(err)
return nil
}
return nil
}))
v.Call("addEventListener", "wheel", js.FuncOf(func(this js.Value, args []js.Value) any {
e := args[0]
e.Call("preventDefault")
if err := u.updateInputFromEvent(e); err != nil {
u.setError(err)
return nil
}
return nil
}))
// Touch
v.Call("addEventListener", "touchstart", js.FuncOf(func(this js.Value, args []js.Value) any {
// Focus the canvas explicitly to activate tha game (#961).
v.Call("focus")
e := args[0]
e.Call("preventDefault")
if err := u.updateInputFromEvent(e); err != nil {
u.setError(err)
return nil
}
return nil
}))
v.Call("addEventListener", "touchend", js.FuncOf(func(this js.Value, args []js.Value) any {
e := args[0]
e.Call("preventDefault")
if err := u.updateInputFromEvent(e); err != nil {
u.setError(err)
return nil
}
return nil
}))
v.Call("addEventListener", "touchmove", js.FuncOf(func(this js.Value, args []js.Value) any {
e := args[0]
e.Call("preventDefault")
if err := u.updateInputFromEvent(e); err != nil {
u.setError(err)
return nil
}
return nil
}))
// Context menu
v.Call("addEventListener", "contextmenu", js.FuncOf(func(this js.Value, args []js.Value) any {
e := args[0]
e.Call("preventDefault")
return nil
}))
// Context
v.Call("addEventListener", "webglcontextlost", js.FuncOf(func(this js.Value, args []js.Value) any {
e := args[0]
e.Call("preventDefault")
window.Get("location").Call("reload")
return nil
}))
// 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
}
go u.appendDroppedFiles(data)
return nil
}))
// Blur
v.Call("addEventListener", "blur", js.FuncOf(func(this js.Value, args []js.Value) any {
u.inputState.resetForBlur()
return nil
}))
}
func (u *UserInterface) appendDroppedFiles(data js.Value) {
u.dropFileM.Lock()
defer u.dropFileM.Unlock()
items := data.Get("items")
for i := 0; i < items.Length(); i++ {
kind := items.Index(i).Get("kind").String()
switch kind {
case "file":
fs := items.Index(i).Call("webkitGetAsEntry").Get("filesystem").Get("root")
u.inputState.DroppedFiles = file.NewFileEntryFS(fs)
return
}
}
}
func (u *UserInterface) forceUpdateOnMinimumFPSMode() {
if u.fpsMode != FPSModeVsyncOffMinimum {
return
}
// updateImpl can block. Use goroutine.
// See https://pkg.go.dev/syscall/js#FuncOf.
go func() {
if err := u.updateImpl(true); err != nil {
u.setError(err)
}
}()
}
func (u *UserInterface) initOnMainThread(options *RunOptions) error {
if !options.InitUnfocused && window.Truthy() {
// 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).
isInIframe := !window.Get("location").Equal(window.Get("parent").Get("location"))
if !isInIframe {
canvas.Call("focus")
}
}
g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{
canvas: canvas,
}, options.GraphicsLibrary)
if err != nil {
return err
}
u.graphicsDriver = g
u.setGraphicsLibrary(lib)
if bodyStyle := document.Get("body").Get("style"); options.ScreenTransparent {
bodyStyle.Set("backgroundColor", "transparent")
} else {
bodyStyle.Set("backgroundColor", "#000")
}
return nil
}
func (u *UserInterface) updateScreenSize() {
if document.Truthy() {
body := document.Get("body")
f := theMonitor.DeviceScaleFactor()
bw := int(body.Get("clientWidth").Float() * f)
bh := int(body.Get("clientHeight").Float() * f)
canvas.Set("width", bw)
canvas.Set("height", bh)
}
}
func (u *UserInterface) readInputState(inputState *InputState) {
u.inputState.copyAndReset(inputState)
u.keyboardLayoutMap = js.Value{}
}
func (u *UserInterface) Window() Window {
return &nullWindow{}
}
type Monitor struct {
deviceScaleFactor float64
}
var theMonitor = &Monitor{}
func (m *Monitor) Name() string {
return ""
}
func (m *Monitor) DeviceScaleFactor() float64 {
if m.deviceScaleFactor != 0 {
return m.deviceScaleFactor
}
ratio := window.Get("devicePixelRatio").Float()
if ratio == 0 {
ratio = 1
}
m.deviceScaleFactor = ratio
return m.deviceScaleFactor
}
func (m *Monitor) Size() (int, int) {
return screen.Get("width").Int(), screen.Get("height").Int()
}
func (u *UserInterface) AppendMonitors(mons []*Monitor) []*Monitor {
return append(mons, theMonitor)
}
func (u *UserInterface) Monitor() *Monitor {
return theMonitor
}
func (u *UserInterface) updateIconIfNeeded() error {
return nil
}
func IsScreenTransparentAvailable() bool {
return true
}
func dipToNativePixels(x float64, scale float64) float64 {
return x
}
+202
View File
@@ -0,0 +1,202 @@
// Copyright 2016 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.
//go:build (freebsd || (linux && !android) || netbsd || openbsd) && !nintendosdk && !playstation5
package ui
import (
"errors"
"fmt"
"runtime"
"github.com/jezek/xgb"
"github.com/jezek/xgb/randr"
"github.com/jezek/xgb/xproto"
"github.com/hajimehoshi/ebiten/v2/internal/glfw"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/opengl"
)
func (u *UserInterface) initializePlatform() error {
return nil
}
type graphicsDriverCreatorImpl struct {
transparent bool
}
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
graphics, err := g.newOpenGL()
return graphics, GraphicsLibraryOpenGL, err
}
func (*graphicsDriverCreatorImpl) newOpenGL() (graphicsdriver.Graphics, error) {
return opengl.NewGraphics()
}
func (*graphicsDriverCreatorImpl) newDirectX() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: DirectX is not supported in this environment")
}
func (*graphicsDriverCreatorImpl) newMetal() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: Metal is not supported in this environment")
}
func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: PlayStation 5 is not supported in this environment")
}
// glfwMonitorSizeInGLFWPixels must be called from the main thread.
func glfwMonitorSizeInGLFWPixels(m *glfw.Monitor) (int, int, error) {
vm, err := m.GetVideoMode()
if err != nil {
return 0, 0, err
}
physWidth, physHeight := vm.Width, vm.Height
// TODO: if glfw/glfw#1961 gets fixed, this function may need revising.
// In case GLFW decides to switch to returning logical pixels, we can just return 1.
// Note: GLFW currently returns physical pixel sizes,
// but we need to predict the window system-side size of the fullscreen window
// for Ebitengine's `(*Monitor).Size()` public API.
// Also at the moment we need this prior to switching to fullscreen, but that might be replaceable.
// So this function computes the ratio of physical per logical pixels.
xconn, err := xgb.NewConn()
if err != nil {
// No X11 connection?
// Assume we're on pure Wayland then.
// GLFW/Wayland shouldn't be having this issue.
return physWidth, physHeight, nil
}
defer xconn.Close()
if err := randr.Init(xconn); err != nil {
// No RANDR extension? No problem.
return physWidth, physHeight, nil
}
root := xproto.Setup(xconn).DefaultScreen(xconn).Root
res, err := randr.GetScreenResourcesCurrent(xconn, root).Reply()
if err != nil {
// Likely means RANDR is not working. No problem.
return physWidth, physHeight, nil
}
monitorX, monitorY, err := m.GetPos()
if err != nil {
// TODO: Is it OK to ignore this error?
return physWidth, physHeight, nil
}
for _, crtc := range res.Crtcs[:res.NumCrtcs] {
info, err := randr.GetCrtcInfo(xconn, crtc, res.ConfigTimestamp).Reply()
if err != nil {
// This Crtc is bad. Maybe just got disconnected?
continue
}
if info.NumOutputs == 0 {
// This Crtc is not connected to any output.
// In other words, a disabled monitor.
continue
}
if int(info.X) == monitorX && int(info.Y) == monitorY {
return int(info.Width), int(info.Height), nil
}
}
// Monitor not known to XRandR. Weird.
return physWidth, physHeight, nil
}
func dipFromGLFWPixel(x float64, deviceScaleFactor float64) float64 {
return x / deviceScaleFactor
}
func dipToGLFWPixel(x float64, deviceScaleFactor float64) float64 {
return x * deviceScaleFactor
}
func (u *UserInterface) adjustWindowPosition(x, y int, monitor *Monitor) (int, int) {
return x, y
}
func initialMonitorByOS() (*Monitor, error) {
xconn, err := xgb.NewConn()
if err != nil {
// Assume we're on pure Wayland then.
return nil, nil
}
defer xconn.Close()
root := xproto.Setup(xconn).DefaultScreen(xconn).Root
rep, err := xproto.QueryPointer(xconn, root).Reply()
if err != nil {
return nil, err
}
x, y := int(rep.RootX), int(rep.RootY)
// Find the monitor including the cursor.
return theMonitors.monitorFromPosition(x, y), nil
}
func monitorFromWindowByOS(_ *glfw.Window) (*Monitor, error) {
// TODO: Implement this correctly. (#1119).
return nil, nil
}
func (u *UserInterface) nativeWindow() (uintptr, error) {
// TODO: Implement this.
return 0, nil
}
func (u *UserInterface) isNativeFullscreen() (bool, error) {
return false, nil
}
func (u *UserInterface) isNativeFullscreenAvailable() bool {
return false
}
func (u *UserInterface) setNativeFullscreen(fullscreen bool) error {
panic(fmt.Sprintf("ui: setNativeFullscreen is not implemented in this environment: %s", runtime.GOOS))
}
func (u *UserInterface) adjustViewSizeAfterFullscreen() error {
return nil
}
func (u *UserInterface) setWindowResizingModeForOS(mode WindowResizingMode) error {
return nil
}
func initializeWindowAfterCreation(w *glfw.Window) error {
// Show the window once before getting the position of the window.
// On Linux/Unix, the window position is not reliable before showing.
if err := w.Show(); err != nil {
return err
}
// Hiding the window makes the position unreliable again. Do not call w.Hide() here (#1829)
// Calling Hide is problematic especially on XWayland and/or Sway.
// Apparently the window state is inconsistent just after the window is created, but we are not sure.
// For more details, see the discussion in #1829.
return nil
}
func (u *UserInterface) skipTaskbar() error {
return nil
}
+328
View File
@@ -0,0 +1,328 @@
// Copyright 2016 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.
//go:build android || ios
package ui
import (
stdcontext "context"
"fmt"
"runtime"
"runtime/debug"
"sync"
"sync/atomic"
"github.com/hajimehoshi/ebiten/v2/internal/gamepad"
"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/hook"
)
var (
// renderCh receives when updating starts.
renderCh = make(chan struct{})
// renderEndCh receives when updating finishes.
renderEndCh = make(chan struct{})
)
func (u *UserInterface) init() error {
u.userInterfaceImpl = userInterfaceImpl{
foreground: 1,
graphicsLibraryInitCh: make(chan struct{}),
errCh: make(chan error),
// Give a default outside size so that the game can start without initializing them.
outsideWidth: 640,
outsideHeight: 480,
}
return nil
}
// Update is called from mobile/ebitenmobileview.
//
// Update must be called on the rendering thread.
func (u *UserInterface) Update() error {
select {
case err := <-u.errCh:
return err
default:
}
if !u.IsFocused() {
return nil
}
if err := gamepad.Update(); err != nil {
return err
}
ctx, cancel := stdcontext.WithCancel(stdcontext.Background())
defer cancel()
renderCh <- struct{}{}
go func() {
<-renderEndCh
cancel()
}()
graphicscommand.LoopRenderThread(ctx)
return nil
}
type userInterfaceImpl struct {
graphicsDriver graphicsdriver.Graphics
graphicsLibraryInitCh chan struct{}
outsideWidth float64
outsideHeight float64
foreground int32
errCh chan error
context *context
inputState InputState
touches []TouchForInput
fpsMode int32
renderRequester RenderRequester
m sync.RWMutex
}
func (u *UserInterface) SetForeground(foreground bool) error {
var v int32
if foreground {
v = 1
}
atomic.StoreInt32(&u.foreground, v)
if foreground {
return hook.ResumeAudio()
} else {
return hook.SuspendAudio()
}
}
func (u *UserInterface) Run(game Game, options *RunOptions) error {
return fmt.Errorf("internal/ui: Run is not implemented for GOOS=%s", runtime.GOOS)
}
func (u *UserInterface) RunWithoutMainLoop(game Game, options *RunOptions) {
go func() {
if err := u.runMobile(game, options); err != nil {
u.errCh <- err
}
}()
}
func (u *UserInterface) runMobile(game Game, options *RunOptions) (err error) {
// Convert the panic to a regular error so that Java/Objective-C layer can treat this easily e.g., for
// Crashlytics. A panic is treated as SIGABRT, and there is no way to handle this on Java/Objective-C layer
// unfortunately.
// TODO: Panic on other goroutines cannot be handled here.
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v\n%s", r, string(debug.Stack()))
}
}()
graphicscommand.SetOSThreadAsRenderThread()
u.setRunning(true)
defer u.setRunning(false)
u.context = newContext(game)
g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{}, options.GraphicsLibrary)
if err != nil {
return err
}
u.graphicsDriver = g
u.setGraphicsLibrary(lib)
close(u.graphicsLibraryInitCh)
for {
if err := u.update(); err != nil {
return err
}
}
}
// outsideSize must be called on the same goroutine as update().
func (u *UserInterface) outsideSize() (float64, float64) {
u.m.RLock()
defer u.m.RUnlock()
return u.outsideWidth, u.outsideHeight
}
func (u *UserInterface) update() error {
<-renderCh
defer func() {
renderEndCh <- struct{}{}
}()
w, h := u.outsideSize()
if err := u.context.updateFrame(u.graphicsDriver, w, h, theMonitor.DeviceScaleFactor(), u); err != nil {
return err
}
return nil
}
// SetOutsideSize is called from mobile/ebitenmobileview.
//
// SetOutsideSize is concurrent safe.
func (u *UserInterface) SetOutsideSize(outsideWidth, outsideHeight float64) {
u.m.Lock()
defer u.m.Unlock()
if u.outsideWidth != outsideWidth || u.outsideHeight != outsideHeight {
u.outsideWidth = outsideWidth
u.outsideHeight = outsideHeight
}
}
func (u *UserInterface) CursorMode() CursorMode {
return CursorModeHidden
}
func (u *UserInterface) SetCursorMode(mode CursorMode) {
// Do nothing
}
func (u *UserInterface) CursorShape() CursorShape {
return CursorShapeDefault
}
func (u *UserInterface) SetCursorShape(shape CursorShape) {
// Do nothing
}
func (u *UserInterface) IsFullscreen() bool {
return false
}
func (u *UserInterface) SetFullscreen(fullscreen bool) {
// Do nothing
}
func (u *UserInterface) IsFocused() bool {
return atomic.LoadInt32(&u.foreground) != 0
}
func (u *UserInterface) IsRunnableOnUnfocused() bool {
return false
}
func (u *UserInterface) SetRunnableOnUnfocused(runnableOnUnfocused bool) {
// Do nothing
}
func (u *UserInterface) FPSMode() FPSModeType {
return FPSModeType(atomic.LoadInt32(&u.fpsMode))
}
func (u *UserInterface) SetFPSMode(mode FPSModeType) {
atomic.StoreInt32(&u.fpsMode, int32(mode))
u.updateExplicitRenderingModeIfNeeded(mode)
}
func (u *UserInterface) updateExplicitRenderingModeIfNeeded(fpsMode FPSModeType) {
if u.renderRequester == nil {
return
}
u.renderRequester.SetExplicitRenderingMode(fpsMode == FPSModeVsyncOffMinimum)
}
func (u *UserInterface) readInputState(inputState *InputState) {
u.m.Lock()
defer u.m.Unlock()
u.inputState.copyAndReset(inputState)
}
func (u *UserInterface) Window() Window {
return &nullWindow{}
}
type Monitor struct {
deviceScaleFactor float64
deviceScaleFactorOnce sync.Once
m sync.Mutex
}
var theMonitor = &Monitor{}
func (m *Monitor) Name() string {
return ""
}
func (m *Monitor) DeviceScaleFactor() float64 {
m.m.Lock()
defer m.m.Unlock()
// The device scale factor can be obtained after the main function starts, especially on Android.
// Initialize this lazily.
m.deviceScaleFactorOnce.Do(func() {
// Assume that the device scale factor never changes on mobiles.
m.deviceScaleFactor = deviceScaleFactorImpl()
})
return m.deviceScaleFactor
}
func (m *Monitor) Size() (int, int) {
// TODO: Return a valid value.
return 0, 0
}
func (u *UserInterface) AppendMonitors(mons []*Monitor) []*Monitor {
return append(mons, theMonitor)
}
func (u *UserInterface) Monitor() *Monitor {
return theMonitor
}
func (u *UserInterface) UpdateInput(keys map[Key]struct{}, runes []rune, touches []TouchForInput) {
u.updateInputStateFromOutside(keys, runes, touches)
if FPSModeType(atomic.LoadInt32(&u.fpsMode)) == FPSModeVsyncOffMinimum {
u.renderRequester.RequestRenderIfNeeded()
}
}
type RenderRequester interface {
SetExplicitRenderingMode(explicitRendering bool)
RequestRenderIfNeeded()
}
func (u *UserInterface) SetRenderRequester(renderRequester RenderRequester) {
u.renderRequester = renderRequester
u.updateExplicitRenderingModeIfNeeded(FPSModeType(atomic.LoadInt32(&u.fpsMode)))
}
func (u *UserInterface) ScheduleFrame() {
if u.renderRequester != nil && FPSModeType(atomic.LoadInt32(&u.fpsMode)) == FPSModeVsyncOffMinimum {
u.renderRequester.RequestRenderIfNeeded()
}
}
func (u *UserInterface) updateIconIfNeeded() error {
return nil
}
func IsScreenTransparentAvailable() bool {
return false
}
+187
View File
@@ -0,0 +1,187 @@
// Copyright 2021 The Ebiten Authors
//
// 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.
//go:build nintendosdk
package ui
// #include "init_nintendosdk.h"
// #include "input_nintendosdk.h"
import "C"
import (
"errors"
"runtime"
"sync"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/opengl"
)
type graphicsDriverCreatorImpl struct {
nativeWindow C.NativeWindowType
}
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
graphics, err := g.newOpenGL()
return graphics, GraphicsLibraryOpenGL, err
}
func (g *graphicsDriverCreatorImpl) newOpenGL() (graphicsdriver.Graphics, error) {
return opengl.NewGraphics(uintptr(g.nativeWindow))
}
func (*graphicsDriverCreatorImpl) newDirectX() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: DirectX is not supported in this environment")
}
func (*graphicsDriverCreatorImpl) newMetal() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: Metal is not supported in this environment")
}
func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: PlayStation 5 is not supported in this environment")
}
func init() {
runtime.LockOSThread()
}
type userInterfaceImpl struct {
graphicsDriver graphicsdriver.Graphics
context *context
inputState InputState
nativeTouches []C.struct_Touch
m sync.Mutex
}
func (u *UserInterface) init() error {
return nil
}
func (u *UserInterface) initOnMainThread(options *RunOptions) error {
n := C.ebitengine_Initialize()
g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{
nativeWindow: n,
}, options.GraphicsLibrary)
if err != nil {
return err
}
u.graphicsDriver = g
u.setGraphicsLibrary(lib)
initializeProfiler()
return nil
}
func (u *UserInterface) loopGame() error {
for {
recordProfilerHeartbeat()
if err := u.context.updateFrame(u.graphicsDriver, float64(C.kScreenWidth), float64(C.kScreenHeight), theMonitor.DeviceScaleFactor(), u); err != nil {
return err
}
}
}
func (*UserInterface) IsFocused() bool {
return true
}
func (u *UserInterface) readInputState(inputState *InputState) {
u.m.Lock()
defer u.m.Unlock()
u.inputState.copyAndReset(inputState)
}
func (*UserInterface) CursorMode() CursorMode {
return CursorModeHidden
}
func (*UserInterface) SetCursorMode(mode CursorMode) {
}
func (*UserInterface) CursorShape() CursorShape {
return CursorShapeDefault
}
func (*UserInterface) SetCursorShape(shape CursorShape) {
}
func (*UserInterface) IsFullscreen() bool {
return false
}
func (*UserInterface) SetFullscreen(fullscreen bool) {
}
func (*UserInterface) IsRunnableOnUnfocused() bool {
return false
}
func (*UserInterface) SetRunnableOnUnfocused(runnableOnUnfocused bool) {
}
func (*UserInterface) FPSMode() FPSModeType {
return FPSModeVsyncOn
}
func (*UserInterface) SetFPSMode(mode FPSModeType) {
}
func (*UserInterface) ScheduleFrame() {
}
func (*UserInterface) Window() Window {
return &nullWindow{}
}
func (u *UserInterface) updateIconIfNeeded() error {
return nil
}
type Monitor struct{}
var theMonitor = &Monitor{}
func (m *Monitor) Name() string {
return ""
}
func (m *Monitor) DeviceScaleFactor() float64 {
return 1
}
func (m *Monitor) Size() (int, int) {
return int(C.kScreenWidth), int(C.kScreenHeight)
}
func (u *UserInterface) AppendMonitors(mons []*Monitor) []*Monitor {
return append(mons, theMonitor)
}
func (u *UserInterface) Monitor() *Monitor {
return theMonitor
}
func IsScreenTransparentAvailable() bool {
return false
}
func dipToNativePixels(x float64, scale float64) float64 {
return x
}
+180
View File
@@ -0,0 +1,180 @@
// Copyright 2023 The Ebitengine Authors
//
// 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.
//go:build playstation5
package ui
import (
"errors"
"image"
"runtime"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/playstation5"
)
type graphicsDriverCreatorImpl struct{}
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
graphics, err := g.newPlayStation5()
return graphics, GraphicsLibraryPlayStation5, err
}
func (*graphicsDriverCreatorImpl) newOpenGL() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: OpenGL is not supported in this environment")
}
func (*graphicsDriverCreatorImpl) newDirectX() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: DirectX is not supported in this environment")
}
func (*graphicsDriverCreatorImpl) newMetal() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: Metal is not supported in this environment")
}
func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, error) {
return playstation5.NewGraphics()
}
const (
// TODO: Get this value from the SDK.
screenWidth = 3840
screenHeight = 2160
)
func init() {
runtime.LockOSThread()
}
type userInterfaceImpl struct {
graphicsDriver graphicsdriver.Graphics
context *context
}
func (u *UserInterface) init() error {
return nil
}
func (u *UserInterface) initOnMainThread(options *RunOptions) error {
g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{}, options.GraphicsLibrary)
if err != nil {
return err
}
u.graphicsDriver = g
u.setGraphicsLibrary(lib)
return nil
}
func (u *UserInterface) loopGame() error {
for {
if err := u.context.updateFrame(u.graphicsDriver, screenWidth, screenHeight, theMonitor.DeviceScaleFactor(), u); err != nil {
return err
}
}
return nil
}
func (*UserInterface) IsFocused() bool {
return true
}
func (u *UserInterface) readInputState(inputState *InputState) {
// TODO: Implement this.
}
func (*UserInterface) CursorMode() CursorMode {
return CursorModeHidden
}
func (*UserInterface) SetCursorMode(mode CursorMode) {
}
func (*UserInterface) CursorShape() CursorShape {
return CursorShapeDefault
}
func (*UserInterface) SetCursorShape(shape CursorShape) {
}
func (*UserInterface) IsFullscreen() bool {
return false
}
func (*UserInterface) SetFullscreen(fullscreen bool) {
}
func (*UserInterface) IsRunnableOnUnfocused() bool {
return false
}
func (*UserInterface) SetRunnableOnUnfocused(runnableOnUnfocused bool) {
}
func (*UserInterface) FPSMode() FPSModeType {
return FPSModeVsyncOn
}
func (*UserInterface) SetFPSMode(mode FPSModeType) {
}
func (*UserInterface) ScheduleFrame() {
}
func (*UserInterface) Window() Window {
return &nullWindow{}
}
func (u *UserInterface) updateIconIfNeeded() error {
return nil
}
type Monitor struct{}
var theMonitor = &Monitor{}
func (m *Monitor) Bounds() image.Rectangle {
// TODO: This should return the available viewport dimensions.
return image.Rectangle{}
}
func (m *Monitor) Name() string {
return ""
}
func (m *Monitor) DeviceScaleFactor() float64 {
return 1
}
func (m *Monitor) Size() (int, int) {
return screenWidth, screenHeight
}
func (u *UserInterface) AppendMonitors(mons []*Monitor) []*Monitor {
return append(mons, theMonitor)
}
func (u *UserInterface) Monitor() *Monitor {
return theMonitor
}
func IsScreenTransparentAvailable() bool {
return false
}
func dipToNativePixels(x float64, scale float64) float64 {
return x
}
+254
View File
@@ -0,0 +1,254 @@
// Copyright 2016 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.
package ui
import (
"errors"
"fmt"
"runtime"
"syscall"
"golang.org/x/sys/windows"
"github.com/hajimehoshi/ebiten/v2/internal/glfw"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/directx"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/opengl"
"github.com/hajimehoshi/ebiten/v2/internal/microsoftgdk"
"github.com/hajimehoshi/ebiten/v2/internal/winver"
)
func (u *UserInterface) initializePlatform() error {
return nil
}
type graphicsDriverCreatorImpl struct {
transparent bool
}
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
var dxErr error
var glErr error
if winver.IsWindows10OrGreater() {
d, err := g.newDirectX()
if err == nil {
return d, GraphicsLibraryDirectX, nil
}
dxErr = err
o, err := g.newOpenGL()
if err == nil {
return o, GraphicsLibraryOpenGL, nil
}
glErr = err
} else {
// Creating a swap chain on an older machine than Windows 10 might fail (#2613).
// Prefer OpenGL to DirectX.
o, err := g.newOpenGL()
if err == nil {
return o, GraphicsLibraryOpenGL, nil
}
glErr = err
// Initializing OpenGL can fail, though this is pretty rare.
d, err := g.newDirectX()
if err == nil {
return d, GraphicsLibraryDirectX, nil
}
dxErr = err
}
return nil, GraphicsLibraryUnknown, fmt.Errorf("ui: failed to choose graphics drivers: DirectX: %v, OpenGL: %v", dxErr, glErr)
}
func (*graphicsDriverCreatorImpl) newOpenGL() (graphicsdriver.Graphics, error) {
return opengl.NewGraphics()
}
func (g *graphicsDriverCreatorImpl) newDirectX() (graphicsdriver.Graphics, error) {
if g.transparent {
return nil, errors.New("ui: DirectX is not available with a transparent window")
}
return directx.NewGraphics()
}
func (*graphicsDriverCreatorImpl) newMetal() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: Metal is not supported in this environment")
}
func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, error) {
return nil, errors.New("ui: PlayStation 5 is not supported in this environment")
}
// glfwMonitorSizeInGLFWPixels must be called from the main thread.
func glfwMonitorSizeInGLFWPixels(m *glfw.Monitor) (int, int, error) {
vm, err := m.GetVideoMode()
if err != nil {
return 0, 0, err
}
return vm.Width, vm.Height, nil
}
func dipFromGLFWPixel(x float64, deviceScaleFactor float64) float64 {
return x / deviceScaleFactor
}
func dipToGLFWPixel(x float64, deviceScaleFactor float64) float64 {
return x * deviceScaleFactor
}
func (u *UserInterface) adjustWindowPosition(x, y int, monitor *Monitor) (int, int) {
if microsoftgdk.IsXbox() {
return x, y
}
mx := monitor.boundsInGLFWPixels.Min.X
my := monitor.boundsInGLFWPixels.Min.Y
// As the video width/height might be wrong,
// adjust x/y at least to enable to handle the window (#328)
if x < mx {
x = mx
}
t, err := _GetSystemMetrics(_SM_CYCAPTION)
if err != nil {
panic(err)
}
if y < my+int(t) {
y = my + int(t)
}
return x, y
}
func initialMonitorByOS() (*Monitor, error) {
if microsoftgdk.IsXbox() {
return theMonitors.primaryMonitor(), nil
}
px, py, err := _GetCursorPos()
if err != nil {
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
return nil, nil
}
return nil, err
}
x, y := int(px), int(py)
// Find the monitor including the cursor.
return theMonitors.monitorFromPosition(x, y), nil
}
func monitorFromWindowByOS(w *glfw.Window) (*Monitor, error) {
if microsoftgdk.IsXbox() {
return theMonitors.primaryMonitor(), nil
}
window, err := w.GetWin32Window()
if err != nil {
return nil, err
}
return monitorFromWin32Window(window), nil
}
func monitorFromWin32Window(w windows.HWND) *Monitor {
// Get the current monitor by the window handle instead of the window position. It is because the window
// position is not reliable in some cases e.g. when the window is put across multiple monitors.
m := _MonitorFromWindow(w, _MONITOR_DEFAULTTONEAREST)
if m == 0 {
// monitorFromWindow can return error on Wine. Ignore this.
return nil
}
mi, err := _GetMonitorInfoW(m)
if err != nil {
panic(err)
}
x, y := int(mi.rcMonitor.left), int(mi.rcMonitor.top)
for _, m := range theMonitors.append(nil) {
mx := m.boundsInGLFWPixels.Min.X
my := m.boundsInGLFWPixels.Min.Y
if mx == x && my == y {
return m
}
}
return nil
}
func (u *UserInterface) nativeWindow() (uintptr, error) {
w, err := u.window.GetWin32Window()
return uintptr(w), err
}
func (u *UserInterface) isNativeFullscreen() (bool, error) {
return false, nil
}
func (u *UserInterface) isNativeFullscreenAvailable() bool {
return false
}
func (u *UserInterface) setNativeFullscreen(fullscreen bool) error {
panic(fmt.Sprintf("ui: setNativeFullscreen is not implemented in this environment: %s", runtime.GOOS))
}
func (u *UserInterface) adjustViewSizeAfterFullscreen() error {
return nil
}
func (u *UserInterface) setWindowResizingModeForOS(mode WindowResizingMode) error {
return nil
}
func initializeWindowAfterCreation(w *glfw.Window) error {
return nil
}
func (u *UserInterface) skipTaskbar() error {
// S_FALSE is returned when CoInitializeEx is nested. This is a successful case.
if err := windows.CoInitializeEx(0, windows.COINIT_MULTITHREADED); err != nil && !errors.Is(err, syscall.Errno(windows.S_FALSE)) {
return err
}
// CoUninitialize should be called even when CoInitializeEx returns S_FALSE.
defer windows.CoUninitialize()
ptr, err := _CoCreateInstance(&_CLSID_TaskbarList, nil, _CLSCTX_SERVER, &_IID_ITaskbarList)
if err != nil {
return err
}
t := (*_ITaskbarList)(ptr)
defer t.Release()
w, err := u.window.GetWin32Window()
if err != nil {
return err
}
if err := t.DeleteTab(w); err != nil {
return err
}
return nil
}
func init() {
if microsoftgdk.IsXbox() {
// TimeBeginPeriod might not be defined in Xbox.
return
}
// Use a better timer resolution (golang/go#44343).
// An error is ignored. The application is still valid even if a higher resolution timer is not available.
// TODO: This might not be necessary from Go 1.23.
_ = windows.TimeBeginPeriod(1)
}
+130
View File
@@ -0,0 +1,130 @@
// Copyright 2022 The Ebiten Authors
//
// 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.
package ui
import (
"image"
)
type Window interface {
IsDecorated() bool
SetDecorated(decorated bool)
ResizingMode() WindowResizingMode
SetResizingMode(mode WindowResizingMode)
SetMonitor(*Monitor)
Position() (int, int)
SetPosition(x, y int)
Size() (int, int)
SetSize(width, height int)
SizeLimits() (minw, minh, maxw, maxh int)
SetSizeLimits(minw, minh, maxw, maxh int)
IsFloating() bool
SetFloating(floating bool)
Maximize()
IsMaximized() bool
Minimize()
IsMinimized() bool
SetIcon(iconImages []image.Image)
SetTitle(title string)
Restore()
SetClosingHandled(handled bool)
IsClosingHandled() bool
SetMousePassthrough(enabled bool)
IsMousePassthrough() bool
}
type nullWindow struct{}
func (*nullWindow) IsDecorated() bool {
return false
}
func (*nullWindow) SetDecorated(decorated bool) {
}
func (*nullWindow) ResizingMode() WindowResizingMode {
return WindowResizingModeDisabled
}
func (*nullWindow) SetResizingMode(mode WindowResizingMode) {
}
func (*nullWindow) SetMonitor(monitor *Monitor) {
}
func (*nullWindow) Position() (int, int) {
return 0, 0
}
func (*nullWindow) SetPosition(x, y int) {
}
func (*nullWindow) Size() (int, int) {
return 0, 0
}
func (*nullWindow) SetSize(width, height int) {
}
func (*nullWindow) SizeLimits() (minw, minh, maxw, maxh int) {
return -1, -1, -1, -1
}
func (*nullWindow) SetSizeLimits(minw, minh, maxw, maxh int) {
}
func (*nullWindow) IsFloating() bool {
return false
}
func (*nullWindow) SetFloating(floating bool) {
}
func (*nullWindow) Maximize() {
}
func (*nullWindow) IsMaximized() bool {
return false
}
func (*nullWindow) Minimize() {
}
func (*nullWindow) IsMinimized() bool {
return false
}
func (*nullWindow) SetIcon(iconImages []image.Image) {
}
func (*nullWindow) SetTitle(title string) {
}
func (*nullWindow) Restore() {
}
func (*nullWindow) SetClosingHandled(handled bool) {
}
func (*nullWindow) IsClosingHandled() bool {
return false
}
func (*nullWindow) SetMousePassthrough(enabled bool) {
}
func (*nullWindow) IsMousePassthrough() bool {
return false
}
+508
View File
@@ -0,0 +1,508 @@
// Copyright 2019 The Ebiten Authors
//
// 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.
//go:build !android && !ios && !js && !nintendosdk && !playstation5
package ui
import (
"image"
"runtime"
"github.com/hajimehoshi/ebiten/v2/internal/glfw"
)
type glfwWindow struct {
ui *UserInterface
}
func (w *glfwWindow) IsDecorated() bool {
if w.ui.isTerminated() {
return false
}
if !w.ui.isRunning() {
return w.ui.isInitWindowDecorated()
}
var v bool
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
a, err := w.ui.window.GetAttrib(glfw.Decorated)
if err != nil {
w.ui.setError(err)
return
}
v = a == glfw.True
})
return v
}
func (w *glfwWindow) SetDecorated(decorated bool) {
if w.ui.isTerminated() {
return
}
if !w.ui.isRunning() {
w.ui.setInitWindowDecorated(decorated)
return
}
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
if err := w.ui.setWindowDecorated(decorated); err != nil {
w.ui.setError(err)
return
}
})
}
func (w *glfwWindow) ResizingMode() WindowResizingMode {
if w.ui.isTerminated() {
return 0
}
if !w.ui.isRunning() {
w.ui.m.Lock()
mode := w.ui.windowResizingMode
w.ui.m.Unlock()
return mode
}
var mode WindowResizingMode
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
mode = w.ui.windowResizingMode
})
return mode
}
func (w *glfwWindow) SetResizingMode(mode WindowResizingMode) {
if w.ui.isTerminated() {
return
}
if !w.ui.isRunning() {
w.ui.m.Lock()
w.ui.windowResizingMode = mode
w.ui.m.Unlock()
return
}
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
if err := w.ui.setWindowResizingMode(mode); err != nil {
w.ui.setError(err)
return
}
})
}
func (w *glfwWindow) IsFloating() bool {
if w.ui.isTerminated() {
return false
}
if !w.ui.isRunning() {
return w.ui.isInitWindowFloating()
}
var v bool
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
a, err := w.ui.window.GetAttrib(glfw.Floating)
if err != nil {
w.ui.setError(err)
return
}
v = a == glfw.True
})
return v
}
func (w *glfwWindow) SetFloating(floating bool) {
if w.ui.isTerminated() {
return
}
if !w.ui.isRunning() {
w.ui.setInitWindowFloating(floating)
return
}
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
if err := w.ui.setWindowFloating(floating); err != nil {
w.ui.setError(err)
return
}
})
}
func (w *glfwWindow) IsMaximized() bool {
if w.ui.isTerminated() {
return false
}
if !w.ui.isRunning() {
return w.ui.isInitWindowMaximized()
}
if w.ResizingMode() != WindowResizingModeEnabled {
return false
}
var v bool
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
m, err := w.ui.isWindowMaximized()
if err != nil {
w.ui.setError(err)
return
}
v = m
})
return v
}
func (w *glfwWindow) Maximize() {
if w.ui.isTerminated() {
return
}
// Do not allow maximizing the window when the window is not resizable.
// On Windows, it is possible to restore the window from being maximized by mouse-dragging,
// and this can be an unexpected behavior (#1990).
if w.ResizingMode() != WindowResizingModeEnabled {
return
}
if !w.ui.isWindowMaximizable() {
return
}
if !w.ui.isRunning() {
w.ui.setInitWindowMaximized(true)
return
}
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
if err := w.ui.maximizeWindow(); err != nil {
w.ui.setError(err)
return
}
})
}
func (w *glfwWindow) IsMinimized() bool {
if !w.ui.isRunning() {
return false
}
var v bool
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
a, err := w.ui.window.GetAttrib(glfw.Iconified)
if err != nil {
w.ui.setError(err)
return
}
v = a == glfw.True
})
return v
}
func (w *glfwWindow) Minimize() {
if !w.ui.isRunning() {
// Do nothing
return
}
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
if err := w.ui.iconifyWindow(); err != nil {
w.ui.setError(err)
return
}
})
}
func (w *glfwWindow) Restore() {
if w.ui.isTerminated() {
return
}
if !w.ui.isWindowMaximizable() {
return
}
if !w.ui.isRunning() {
// Do nothing
return
}
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
if err := w.ui.restoreWindow(); err != nil {
w.ui.setError(err)
return
}
})
}
func (w *glfwWindow) SetMonitor(monitor *Monitor) {
if monitor == nil {
panic("ui: monitor cannot be nil at SetMonitor")
}
if w.ui.isTerminated() {
return
}
if !w.ui.isRunning() {
w.ui.setInitMonitor(monitor)
return
}
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
if err := w.ui.setWindowMonitor(monitor); err != nil {
w.ui.setError(err)
return
}
})
}
func (w *glfwWindow) Position() (int, int) {
if w.ui.isTerminated() {
return 0, 0
}
if !w.ui.isRunning() {
panic("ui: WindowPosition can't be called before the main loop starts")
}
var x, y int
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
f, err := w.ui.isFullscreen()
if err != nil {
w.ui.setError(err)
return
}
var wx, wy int
if f {
wx, wy = w.ui.origWindowPos()
} else {
x, y, err := w.ui.window.GetPos()
if err != nil {
w.ui.setError(err)
return
}
wx, wy = x, y
}
m, err := w.ui.currentMonitor()
if err != nil {
w.ui.setError(err)
return
}
wx -= m.boundsInGLFWPixels.Min.X
wy -= m.boundsInGLFWPixels.Min.Y
s := m.DeviceScaleFactor()
xf := dipFromGLFWPixel(float64(wx), s)
yf := dipFromGLFWPixel(float64(wy), s)
x, y = int(xf), int(yf)
})
return x, y
}
func (w *glfwWindow) SetPosition(x, y int) {
if w.ui.isTerminated() {
return
}
if !w.ui.isRunning() {
w.ui.setInitWindowPositionInDIP(x, y)
return
}
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
m, err := w.ui.currentMonitor()
if err != nil {
w.ui.setError(err)
return
}
if err := w.ui.setWindowPositionInDIP(x, y, m); err != nil {
w.ui.setError(err)
return
}
})
}
func (w *glfwWindow) Size() (int, int) {
if w.ui.isTerminated() {
return 0, 0
}
if !w.ui.isRunning() {
ww, wh := w.ui.getInitWindowSizeInDIP()
return w.ui.adjustWindowSizeBasedOnSizeLimitsInDIP(ww, wh)
}
var ww, wh int
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
// Unlike origWindowPos, origWindow{Width,Height}InDPI are always updated via the callback.
ww = w.ui.origWindowWidthInDIP
wh = w.ui.origWindowHeightInDIP
})
return ww, wh
}
func (w *glfwWindow) SetSize(width, height int) {
if w.ui.isTerminated() {
return
}
if !w.ui.isRunning() {
// If the window is initially maximized, the set size is ignored anyway.
w.ui.setInitWindowSizeInDIP(width, height)
return
}
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
m, err := w.ui.isWindowMaximized()
if err != nil {
w.ui.setError(err)
return
}
if m && runtime.GOOS != "darwin" {
return
}
if err := w.ui.setWindowSizeInDIP(width, height, true); err != nil {
w.ui.setError(err)
return
}
})
}
func (w *glfwWindow) SizeLimits() (minw, minh, maxw, maxh int) {
return w.ui.getWindowSizeLimitsInDIP()
}
func (w *glfwWindow) SetSizeLimits(minw, minh, maxw, maxh int) {
if w.ui.isTerminated() {
return
}
if !w.ui.setWindowSizeLimitsInDIP(minw, minh, maxw, maxh) {
return
}
if !w.ui.isRunning() {
return
}
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
if err := w.ui.updateWindowSizeLimits(); err != nil {
w.ui.setError(err)
return
}
})
}
func (w *glfwWindow) SetIcon(iconImages []image.Image) {
if w.ui.isTerminated() {
return
}
// The icons are actually set at (*UserInterface).loop.
w.ui.setIconImages(iconImages)
}
func (w *glfwWindow) SetTitle(title string) {
if w.ui.isTerminated() {
return
}
if !w.ui.isRunning() {
w.ui.m.Lock()
w.ui.title = title
w.ui.m.Unlock()
return
}
w.ui.title = title
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
if err := w.ui.setWindowTitle(title); err != nil {
w.ui.setError(err)
return
}
})
}
func (w *glfwWindow) SetClosingHandled(handled bool) {
w.ui.setWindowClosingHandled(handled)
}
func (w *glfwWindow) IsClosingHandled() bool {
return w.ui.isWindowClosingHandled()
}
func (w *glfwWindow) SetMousePassthrough(enabled bool) {
if w.ui.isTerminated() {
return
}
if !w.ui.isRunning() {
w.ui.setInitWindowMousePassthrough(enabled)
return
}
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
if err := w.ui.setWindowMousePassthrough(enabled); err != nil {
w.ui.setError(err)
return
}
})
}
func (w *glfwWindow) IsMousePassthrough() bool {
if w.ui.isTerminated() {
return false
}
if !w.ui.isRunning() {
return w.ui.isInitWindowMousePassthrough()
}
var v bool
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
a, err := w.ui.window.GetAttrib(glfw.MousePassthrough)
if err != nil {
w.ui.setError(err)
return
}
v = a == glfw.True
})
return v
}