ebiten: Add CursorShape/SetCursorShape/CursorShapeType

This change adds APIs to enable to use system cursor shapes other
than the default shape (an arrow).

This change doesn't add these cursors since they seem a little
different on macOS from the other platforms.

 * GLFW_HRESIZE_CURSOR
 * GLFW_VRESIZE_CURSOR

Closes #995
This commit is contained in:
Hajime Hoshi 2021-04-11 14:21:38 +09:00
parent 71e899acf3
commit d00d0c8556
11 changed files with 274 additions and 11 deletions

View File

@ -16,12 +16,23 @@ package ebiten
import "github.com/hajimehoshi/ebiten/v2/internal/driver" import "github.com/hajimehoshi/ebiten/v2/internal/driver"
// CursorModeType represents // CursorModeType represents a render and coordinate mode of a mouse cursor.
// a render and coordinate mode of a mouse cursor.
type CursorModeType int type CursorModeType int
// CursorModeTypes
const ( const (
CursorModeVisible CursorModeType = CursorModeType(driver.CursorModeVisible) CursorModeVisible CursorModeType = CursorModeType(driver.CursorModeVisible)
CursorModeHidden CursorModeType = CursorModeType(driver.CursorModeHidden) CursorModeHidden CursorModeType = CursorModeType(driver.CursorModeHidden)
CursorModeCaptured CursorModeType = CursorModeType(driver.CursorModeCaptured) CursorModeCaptured CursorModeType = CursorModeType(driver.CursorModeCaptured)
) )
// CursorShapeType represents a shape of a mouse cursor.
type CursorShapeType int
// CursorShapeTypes
const (
CursorShapeDefault CursorShapeType = CursorShapeType(driver.CursorShapeDefault)
CursorShapeText CursorShapeType = CursorShapeType(driver.CursorShapeText)
CursorShapeCrosshair CursorShapeType = CursorShapeType(driver.CursorShapeCrosshair)
CursorShapePointer CursorShapeType = CursorShapeType(driver.CursorShapePointer)
)

94
examples/cursor/main.go Normal file
View File

@ -0,0 +1,94 @@
// 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.
// +build example
package main
import (
"image"
"image/color"
"log"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)
const (
screenWidth = 640
screenHeight = 480
)
type Game struct {
grids map[image.Rectangle]ebiten.CursorShapeType
gridColors map[image.Rectangle]color.Color
}
func (g *Game) Update() error {
pt := image.Pt(ebiten.CursorPosition())
for r, c := range g.grids {
if pt.In(r) {
ebiten.SetCursorShape(c)
return nil
}
}
ebiten.SetCursorShape(ebiten.CursorShapeDefault)
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
for r, c := range g.gridColors {
ebitenutil.DrawRect(screen, float64(r.Min.X), float64(r.Min.Y), float64(r.Dx()), float64(r.Dy()), c)
}
switch ebiten.CursorShape() {
case ebiten.CursorShapeDefault:
ebitenutil.DebugPrint(screen, "CursorShape: Default")
case ebiten.CursorShapeText:
ebitenutil.DebugPrint(screen, "CursorShape: Text")
case ebiten.CursorShapeCrosshair:
ebitenutil.DebugPrint(screen, "CursorShape: Crosshair")
case ebiten.CursorShapePointer:
ebitenutil.DebugPrint(screen, "CursorShape: Pointer")
}
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return screenWidth, screenHeight
}
func main() {
g := &Game{
grids: map[image.Rectangle]ebiten.CursorShapeType{
image.Rect(100, 100, 200, 300): ebiten.CursorShapeDefault,
image.Rect(200, 100, 300, 300): ebiten.CursorShapeText,
image.Rect(300, 100, 400, 300): ebiten.CursorShapeCrosshair,
image.Rect(400, 100, 500, 300): ebiten.CursorShapePointer,
},
gridColors: map[image.Rectangle]color.Color{},
}
for rect, c := range g.grids {
a := byte(0x40)
if c%2 == 0 {
a += 0x40
}
g.gridColors[rect] = color.Alpha{a}
}
ebiten.SetWindowSize(screenWidth, screenHeight)
ebiten.SetWindowTitle("Cursor (Ebiten Demo)")
if err := ebiten.RunGame(g); err != nil {
log.Fatal(err)
}
}

View File

@ -21,3 +21,12 @@ const (
CursorModeHidden CursorModeHidden
CursorModeCaptured CursorModeCaptured
) )
type CursorShape int
const (
CursorShapeDefault CursorShape = iota
CursorShapeText
CursorShapeCrosshair
CursorShapePointer
)

View File

@ -45,6 +45,9 @@ type UI interface {
CursorMode() CursorMode CursorMode() CursorMode
SetCursorMode(mode CursorMode) SetCursorMode(mode CursorMode)
CursorShape() CursorShape
SetCursorShape(shape CursorShape)
IsFullscreen() bool IsFullscreen() bool
SetFullscreen(fullscreen bool) SetFullscreen(fullscreen bool)

View File

@ -30,6 +30,7 @@ type (
ModifierKey int ModifierKey int
MouseButton int MouseButton int
PeripheralEvent int PeripheralEvent int
StandardCursor int
) )
const ( const (
@ -142,3 +143,12 @@ func (e ErrorCode) String() string {
return fmt.Sprintf("GLFW error code (%d)", e) return fmt.Sprintf("GLFW error code (%d)", e)
} }
} }
const (
ArrowCursor = StandardCursor(0x00036001)
IBeamCursor = StandardCursor(0x00036002)
CrosshairCursor = StandardCursor(0x00036003)
HandCursor = StandardCursor(0x00036004)
HResizeCursor = StandardCursor(0x00036005)
VResizeCursor = StandardCursor(0x00036006)
)

View File

@ -58,6 +58,15 @@ func (w windows) get(win *glfw.Window) *Window {
return ww return ww
} }
type Cursor struct {
c *glfw.Cursor
}
func CreateStandardCursor(shape StandardCursor) *Cursor {
c := glfw.CreateStandardCursor(glfw.StandardCursor(shape))
return &Cursor{c: c}
}
type Monitor struct { type Monitor struct {
m *glfw.Monitor m *glfw.Monitor
} }
@ -163,6 +172,14 @@ func (w *Window) SetCharModsCallback(cbfun CharModsCallback) (previous CharModsC
return nil // TODO return nil // TODO
} }
func (w *Window) SetCursor(cursor *Cursor) {
var c *glfw.Cursor
if cursor != nil {
c = cursor.c
}
w.w.SetCursor(c)
}
func (w *Window) SetFramebufferSizeCallback(cbfun FramebufferSizeCallback) (previous FramebufferSizeCallback) { func (w *Window) SetFramebufferSizeCallback(cbfun FramebufferSizeCallback) (previous FramebufferSizeCallback) {
var gcb glfw.FramebufferSizeCallback var gcb glfw.FramebufferSizeCallback
if cbfun != nil { if cbfun != nil {

View File

@ -65,6 +65,16 @@ func (w glfwWindows) get(win uintptr) *Window {
return ww return ww
} }
type Cursor struct {
c uintptr
}
func CreateStandardCursor(shape StandardCursor) *Cursor {
c := glfwDLL.call("glfwCreateStandardCursor", uintptr(shape))
panicError()
return &Cursor{c: c}
}
type Monitor struct { type Monitor struct {
m uintptr m uintptr
} }
@ -192,6 +202,14 @@ func (w *Window) SetCharModsCallback(cbfun CharModsCallback) (previous CharModsC
return nil // TODO return nil // TODO
} }
func (w *Window) SetCursor(cursor *Cursor) {
var c uintptr
if cursor != nil {
c = cursor.c
}
glfwDLL.call("glfwSetCursor", w.w, c)
}
func (w *Window) SetFramebufferSizeCallback(cbfun FramebufferSizeCallback) (previous FramebufferSizeCallback) { func (w *Window) SetFramebufferSizeCallback(cbfun FramebufferSizeCallback) (previous FramebufferSizeCallback) {
var gcb uintptr var gcb uintptr
if cbfun != nil { if cbfun != nil {

View File

@ -64,6 +64,7 @@ type UserInterface struct {
runnableOnUnfocused bool runnableOnUnfocused bool
vsync bool vsync bool
iconImages []image.Image iconImages []image.Image
cursorShape driver.CursorShape
// err must be accessed from the main thread. // err must be accessed from the main thread.
err error err error
@ -143,10 +144,13 @@ func init() {
cacheMonitors() cacheMonitors()
} }
var glfwSystemCursors = map[driver.CursorShape]*glfw.Cursor{}
func initialize() error { func initialize() error {
if err := glfw.Init(); err != nil { if err := glfw.Init(); err != nil {
return err return err
} }
glfw.WindowHint(glfw.Visible, glfw.False) glfw.WindowHint(glfw.Visible, glfw.False)
glfw.WindowHint(glfw.ClientAPI, glfw.NoAPI) glfw.WindowHint(glfw.ClientAPI, glfw.NoAPI)
@ -168,6 +172,12 @@ func initialize() error {
theUI.initFullscreenWidthInDP = int(fromGLFWMonitorPixel(float64(v.Width), scale)) theUI.initFullscreenWidthInDP = int(fromGLFWMonitorPixel(float64(v.Width), scale))
theUI.initFullscreenHeightInDP = int(fromGLFWMonitorPixel(float64(v.Height), scale)) theUI.initFullscreenHeightInDP = int(fromGLFWMonitorPixel(float64(v.Height), scale))
// Create system cursors. These cursors are destroyed at glfw.Terminate().
glfwSystemCursors[driver.CursorShapeDefault] = nil
glfwSystemCursors[driver.CursorShapeText] = glfw.CreateStandardCursor(glfw.IBeamCursor)
glfwSystemCursors[driver.CursorShapeCrosshair] = glfw.CreateStandardCursor(glfw.CrosshairCursor)
glfwSystemCursors[driver.CursorShapePointer] = glfw.CreateStandardCursor(glfw.HandCursor)
return nil return nil
} }
@ -271,6 +281,21 @@ func (u *UserInterface) setInitCursorMode(mode driver.CursorMode) {
u.m.Unlock() u.m.Unlock()
} }
func (u *UserInterface) getCursorShape() driver.CursorShape {
u.m.RLock()
v := u.cursorShape
u.m.RUnlock()
return v
}
func (u *UserInterface) setCursorShape(shape driver.CursorShape) driver.CursorShape {
u.m.Lock()
old := u.cursorShape
u.cursorShape = shape
u.m.Unlock()
return old
}
func (u *UserInterface) isInitWindowDecorated() bool { func (u *UserInterface) isInitWindowDecorated() bool {
u.m.RLock() u.m.RLock()
v := u.initWindowDecorated v := u.initWindowDecorated
@ -557,6 +582,24 @@ func (u *UserInterface) SetCursorMode(mode driver.CursorMode) {
}) })
} }
func (u *UserInterface) CursorShape() driver.CursorShape {
return u.getCursorShape()
}
func (u *UserInterface) SetCursorShape(shape driver.CursorShape) {
old := u.setCursorShape(shape)
if old == shape {
return
}
if !u.isRunning() {
return
}
_ = u.t.Call(func() error {
u.window.SetCursor(glfwSystemCursors[shape])
return nil
})
}
func (u *UserInterface) DeviceScaleFactor() float64 { func (u *UserInterface) DeviceScaleFactor() float64 {
if !u.isRunning() { if !u.isRunning() {
return devicescale.GetAt(u.initMonitor.GetPos()) return devicescale.GetAt(u.initMonitor.GetPos())
@ -615,6 +658,7 @@ func (u *UserInterface) createWindow() error {
u.window.SetInputMode(glfw.StickyMouseButtonsMode, glfw.True) u.window.SetInputMode(glfw.StickyMouseButtonsMode, glfw.True)
u.window.SetInputMode(glfw.StickyKeysMode, glfw.True) u.window.SetInputMode(glfw.StickyKeysMode, glfw.True)
u.window.SetInputMode(glfw.CursorMode, driverCursorModeToGLFWCursorMode(u.getInitCursorMode())) u.window.SetInputMode(glfw.CursorMode, driverCursorModeToGLFWCursorMode(u.getInitCursorMode()))
u.window.SetCursor(glfwSystemCursors[u.getCursorShape()])
u.window.SetTitle(u.title) u.window.SetTitle(u.title)
// TODO: Set icons // TODO: Set icons

View File

@ -31,11 +31,27 @@ var (
stringTransparent = js.ValueOf("transparent") stringTransparent = js.ValueOf("transparent")
) )
func driverCursorShapeToCSSCursor(cursor driver.CursorShape) string {
switch cursor {
case driver.CursorShapeDefault:
return "default"
case driver.CursorShapeText:
return "text"
case driver.CursorShapeCrosshair:
return "crosshair"
case driver.CursorShapePointer:
return "pointer"
}
return "auto"
}
type UserInterface struct { type UserInterface struct {
runnableOnUnfocused bool runnableOnUnfocused bool
vsync bool vsync bool
running bool running bool
initFocused bool initFocused bool
cursorHidden bool
cursorShape driver.CursorShape
sizeChanged bool sizeChanged bool
contextLost bool contextLost bool
@ -107,10 +123,10 @@ func (u *UserInterface) CursorMode() driver.CursorMode {
return driver.CursorModeHidden return driver.CursorModeHidden
} }
if jsutil.Equal(canvas.Get("style").Get("cursor"), stringNone) { if u.cursorHidden {
return driver.CursorModeVisible return driver.CursorModeHidden
} }
return driver.CursorModeHidden return driver.CursorModeVisible
} }
func (u *UserInterface) SetCursorMode(mode driver.CursorMode) { func (u *UserInterface) SetCursorMode(mode driver.CursorMode) {
@ -118,20 +134,45 @@ func (u *UserInterface) SetCursorMode(mode driver.CursorMode) {
return return
} }
var visible bool
switch mode { switch mode {
case driver.CursorModeVisible: case driver.CursorModeVisible:
visible = true if u.cursorHidden {
return
}
u.cursorHidden = false
case driver.CursorModeHidden: case driver.CursorModeHidden:
visible = false if !u.cursorHidden {
return
}
u.cursorHidden = true
default: default:
return return
} }
if visible { if u.cursorHidden {
canvas.Get("style").Set("cursor", "auto") canvas.Get("style").Set("cursor", stringNone)
} else { } else {
canvas.Get("style").Set("cursor", "none") canvas.Get("style").Set("cursor", driverCursorShapeToCSSCursor(u.cursorShape))
}
}
func (u *UserInterface) CursorShape() driver.CursorShape {
if !canvas.Truthy() {
return driver.CursorShapeDefault
}
return u.cursorShape
}
func (u *UserInterface) SetCursorShape(shape driver.CursorShape) {
if !canvas.Truthy() {
return
}
if u.cursorShape == shape {
return
}
u.cursorShape = shape
if !u.cursorHidden {
canvas.Get("style").Set("cursor", driverCursorShapeToCSSCursor(u.cursorShape))
} }
} }

View File

@ -370,6 +370,14 @@ func (u *UserInterface) SetCursorMode(mode driver.CursorMode) {
// Do nothing // Do nothing
} }
func (u *UserInterface) CursorShape() driver.CursorShape {
return driver.CursorShapeDefault
}
func (u *UserInterface) SetCursorShape(shape driver.CursorShape) {
// Do nothing
}
func (u *UserInterface) IsFullscreen() bool { func (u *UserInterface) IsFullscreen() bool {
return false return false
} }

8
run.go
View File

@ -241,6 +241,14 @@ func SetCursorMode(mode CursorModeType) {
uiDriver().SetCursorMode(driver.CursorMode(mode)) uiDriver().SetCursorMode(driver.CursorMode(mode))
} }
func CursorShape() CursorShapeType {
return CursorShapeType(uiDriver().CursorShape())
}
func SetCursorShape(shape CursorShapeType) {
uiDriver().SetCursorShape(driver.CursorShape(shape))
}
// IsFullscreen reports whether the current mode is fullscreen or not. // IsFullscreen reports whether the current mode is fullscreen or not.
// //
// IsFullscreen always returns false on browsers or mobiles. // IsFullscreen always returns false on browsers or mobiles.