[DO NOT MERGE] ebiten: add SetCursorPosition

Closes #2653
This commit is contained in:
Hajime Hoshi 2024-09-16 01:41:31 +09:00
parent a113687d56
commit cb56906e14
7 changed files with 202 additions and 13 deletions

View File

@ -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)
}
}

View File

@ -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.

View File

@ -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
}

View File

@ -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.
}

View File

@ -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.
}

View File

@ -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.
}

View File

@ -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.
}