diff --git a/examples/setcursorposition/main.go b/examples/setcursorposition/main.go new file mode 100644 index 000000000..835124e54 --- /dev/null +++ b/examples/setcursorposition/main.go @@ -0,0 +1,67 @@ +// 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 main + +import ( + "fmt" + "log" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/ebitenutil" +) + +const ( + screenWidth = 640 + screenHeight = 480 +) + +type Game struct { +} + +func (g *Game) Update() error { + x, y := ebiten.CursorPosition() + if ebiten.IsKeyPressed(ebiten.KeyLeft) || ebiten.IsKeyPressed(ebiten.KeyA) { + x-- + } + if ebiten.IsKeyPressed(ebiten.KeyRight) || ebiten.IsKeyPressed(ebiten.KeyD) { + x++ + } + if ebiten.IsKeyPressed(ebiten.KeyUp) || ebiten.IsKeyPressed(ebiten.KeyW) { + y-- + } + if ebiten.IsKeyPressed(ebiten.KeyDown) || ebiten.IsKeyPressed(ebiten.KeyS) { + y++ + } + ebiten.SetCursorPosition(x, y) + return nil +} + +func (g *Game) Draw(screen *ebiten.Image) { + x, y := ebiten.CursorPosition() + ebitenutil.DebugPrint(screen, fmt.Sprintf("Cursor Position: (%d, %d)\nPress arrow keys or WASD keys.", x, y)) +} + +func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { + return screenWidth, screenHeight +} + +func main() { + ebiten.SetWindowSize(screenWidth, screenHeight) + ebiten.SetWindowTitle("SetCursorPosition (Ebitengine Demo)") + ebiten.SetCursorPosition(screenWidth/2, screenHeight/2) + if err := ebiten.RunGame(&Game{}); err != nil { + log.Fatal(err) + } +} diff --git a/input.go b/input.go index 3ad5d0456..b1019943e 100644 --- a/input.go +++ b/input.go @@ -74,17 +74,27 @@ func KeyName(key Key) string { return ui.Get().KeyName(ui.Key(key)) } -// CursorPosition returns a position of a mouse cursor relative to the game screen (window). The cursor position is -// 'logical' position and this considers the scale of the screen. +// CursorPosition returns a position of a mouse cursor relative to the game screen (window). +// The cursor position is 'logical' position and this considers the scale of the screen. // -// CursorPosition returns (0, 0) before the main loop on desktops and browsers. +// CursorPosition returns (0, 0) or the position set by SetCursorPosition before the main loop on desktops and browsers. // // CursorPosition always returns (0, 0) on mobile native applications. // // CursorPosition is concurrent-safe. func CursorPosition() (x, y int) { - cx, cy := theInputState.cursorPosition() - return int(cx), int(cy) + // For the cursor position, theInputState is not used since the cursor position can be updated by SetCursorPosition. + return ui.Get().CursorPosition() +} + +// SetCursorPosition sets the cursor position. +// The cursor position is 'logical' position and this considers the scale of the screen. +// +// SetCursorPosition works only on desktops. SetCursorPosition does nothing otherwise. +// +// SetCursorPosition is concurrent-safe. +func SetCursorPosition(x, y int) { + ui.Get().SetCursorPosition(x, y) } // Wheel returns x and y offsets of the mouse wheel or touchpad scroll. diff --git a/internal/ui/ui_glfw.go b/internal/ui/ui_glfw.go index bbf035145..15fe15251 100644 --- a/internal/ui/ui_glfw.go +++ b/internal/ui/ui_glfw.go @@ -24,6 +24,7 @@ import ( "os" "runtime" "sync" + "sync/atomic" "time" "github.com/hajimehoshi/ebiten/v2/internal/file" @@ -72,6 +73,8 @@ type userInterfaceImpl struct { initMonitor *Monitor initFullscreen bool initCursorMode CursorMode + initCursorX int + initCursorY int initWindowDecorated bool initWindowPositionXInDIP int initWindowPositionYInDIP int @@ -84,7 +87,7 @@ type userInterfaceImpl struct { initUnfocused bool // bufferOnceSwapped must be accessed from the main thread. - bufferOnceSwapped bool + bufferOnceSwapped atomic.Bool origWindowPosX int origWindowPosY int @@ -133,6 +136,8 @@ func (u *UserInterface) init() error { maxWindowWidthInDIP: glfw.DontCare, maxWindowHeightInDIP: glfw.DontCare, initCursorMode: CursorModeVisible, + initCursorX: invalidPos, + initCursorY: invalidPos, initWindowDecorated: true, initWindowPositionXInDIP: invalidPos, initWindowPositionYInDIP: invalidPos, @@ -409,6 +414,19 @@ func (u *UserInterface) setInitCursorMode(mode CursorMode) { u.m.Unlock() } +func (u *UserInterface) setInitCursorPosition(x, y int) { + u.m.Lock() + defer u.m.Unlock() + u.initCursorX = x + u.initCursorY = y +} + +func (u *UserInterface) getInitCursorPosition() (int, int) { + u.m.RLock() + defer u.m.RUnlock() + return u.initCursorX, u.initCursorY +} + func (u *UserInterface) getCursorShape() CursorShape { u.m.RLock() v := u.cursorShape @@ -1292,14 +1310,22 @@ func (u *UserInterface) update() (float64, float64, error) { } // On macOS, one swapping buffers seems required before entering fullscreen (#2599). - if u.isInitFullscreen() && (u.bufferOnceSwapped || runtime.GOOS != "darwin") { + if u.isInitFullscreen() && (u.bufferOnceSwapped.Load() || runtime.GOOS != "darwin") { if err := u.setFullscreen(true); err != nil { return 0, 0, err } u.setInitFullscreen(false) } - if runtime.GOOS == "darwin" && u.bufferOnceSwapped { + if !u.bufferOnceSwapped.Load() { + if x, y := u.getInitCursorPosition(); x != invalidPos && y != invalidPos { + if err := u.setCursorPosition(x, y); err != nil { + return 0, 0, err + } + } + } + + if runtime.GOOS == "darwin" && u.bufferOnceSwapped.Load() { var err error u.darwinInitOnce.Do(func() { // On macOS, window decoration should be initialized once after buffers are swapped (#2600). @@ -1316,7 +1342,7 @@ func (u *UserInterface) update() (float64, float64, error) { } } - if u.bufferOnceSwapped { + if u.bufferOnceSwapped.Load() { var err error u.showWindowOnce.Do(func() { // Show the window after first buffer swap to avoid flash of white especially on Windows. @@ -1390,7 +1416,7 @@ func (u *UserInterface) update() (float64, float64, error) { // If isRunnableOnUnfocused is false and the window is not focused, wait here. // For the first update, skip this check as the window might not be seen yet in some environments like ChromeOS (#3091). - for !u.isRunnableOnUnfocused() && u.bufferOnceSwapped { + for !u.isRunnableOnUnfocused() && u.bufferOnceSwapped.Load() { // In the initial state on macOS, the window is not shown (#2620). visible, err := u.window.GetAttrib(glfw.Visible) if err != nil { @@ -1493,9 +1519,7 @@ func (u *UserInterface) updateGame() error { } u.bufferOnceSwappedOnce.Do(func() { - u.mainThread.Call(func() { - u.bufferOnceSwapped = true - }) + u.bufferOnceSwapped.Store(true) }) if unfocused { @@ -2178,3 +2202,51 @@ func (u *UserInterface) RunOnMainThread(f func()) { func dipToNativePixels(x float64, scale float64) float64 { return dipToGLFWPixel(x, scale) } + +func (u *UserInterface) CursorPosition() (x, y int) { + if !u.isRunning() || !u.bufferOnceSwapped.Load() { + x, y := u.getInitCursorPosition() + if x == invalidPos || y == invalidPos { + return 0, 0 + } + return x, y + } + + u.m.Lock() + defer u.m.Unlock() + return int(u.inputState.CursorX), int(u.inputState.CursorY) +} + +func (u *UserInterface) SetCursorPosition(x, y int) { + if !u.isRunning() { + u.setInitCursorPosition(x, y) + return + } + u.mainThread.Call(func() { + if err := u.setCursorPosition(x, y); err != nil { + u.setError(err) + return + } + }) +} + +// setCursorPosition must be called from the main thread. +func (u *UserInterface) setCursorPosition(x, y int) error { + m, err := u.currentMonitor() + if err != nil { + return err + } + + s := m.DeviceScaleFactor() + cx, cy := u.context.logicalPositionToClientPosition(float64(x), float64(y), s) + gx := dipToGLFWPixel(cx, s) + gy := dipToGLFWPixel(cy, s) + u.window.SetCursorPos(gx, gy) + + u.m.Lock() + defer u.m.Unlock() + u.inputState.CursorX = float64(x) + u.inputState.CursorY = float64(y) + + return nil +} diff --git a/internal/ui/ui_js.go b/internal/ui/ui_js.go index 3d35ba6d2..f8bcea3ee 100644 --- a/internal/ui/ui_js.go +++ b/internal/ui/ui_js.go @@ -864,3 +864,13 @@ func IsScreenTransparentAvailable() bool { func dipToNativePixels(x float64, scale float64) float64 { return x } + +func (u *UserInterface) CursorPosition() (x, y int) { + u.m.Lock() + defer u.m.Unlock() + return int(u.inputState.CursorX), int(u.inputState.CursorY) +} + +func (u *UserInterface) SetCursorPosition(x, y int) { + // Do nothing. +} diff --git a/internal/ui/ui_mobile.go b/internal/ui/ui_mobile.go index d755798e3..25b90d639 100644 --- a/internal/ui/ui_mobile.go +++ b/internal/ui/ui_mobile.go @@ -337,3 +337,13 @@ func (u *UserInterface) UsesStrictContextRestoration() bool { func IsScreenTransparentAvailable() bool { return false } + +func (u *UserInterface) CursorPosition() (x, y int) { + u.m.Lock() + defer u.m.Unlock() + return int(u.inputState.CursorX), int(u.inputState.CursorY) +} + +func (u *UserInterface) SetCursorPosition(x, y int) { + // Do nothing. +} diff --git a/internal/ui/ui_nintendosdk.go b/internal/ui/ui_nintendosdk.go index 943749b53..eac637947 100644 --- a/internal/ui/ui_nintendosdk.go +++ b/internal/ui/ui_nintendosdk.go @@ -187,3 +187,13 @@ func IsScreenTransparentAvailable() bool { func dipToNativePixels(x float64, scale float64) float64 { return x } + +func (u *UserInterface) CursorPosition() (x, y int) { + u.m.Lock() + defer u.m.Unlock() + return int(u.inputState.CursorX), int(u.inputState.CursorY) +} + +func (u *UserInterface) SetCursorPosition(x, y int) { + // Do nothing. +} diff --git a/internal/ui/ui_playstation5.go b/internal/ui/ui_playstation5.go index c97fd5664..07b72327f 100644 --- a/internal/ui/ui_playstation5.go +++ b/internal/ui/ui_playstation5.go @@ -180,3 +180,13 @@ func IsScreenTransparentAvailable() bool { func dipToNativePixels(x float64, scale float64) float64 { return x } + +func (u *UserInterface) CursorPosition() (x, y int) { + u.m.Lock() + defer u.m.Unlock() + return int(u.inputState.CursorX), int(u.inputState.CursorY) +} + +func (u *UserInterface) SetCursorPosition(x, y int) { + // Do nothing. +}