Skip to content

Commit

Permalink
ebiten: Add CursorShape/SetCursorShape/CursorShapeType
Browse files Browse the repository at this point in the history
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 hajimehoshi#995
  • Loading branch information
hajimehoshi committed Apr 15, 2021
1 parent 71e899a commit d00d0c8
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 11 deletions.
15 changes: 13 additions & 2 deletions cursormode.go → cursor.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,23 @@ package ebiten

import "github.com/hajimehoshi/ebiten/v2/internal/driver"

// CursorModeType represents
// a render and coordinate mode of a mouse cursor.
// CursorModeType represents a render and coordinate mode of a mouse cursor.
type CursorModeType int

// CursorModeTypes
const (
CursorModeVisible CursorModeType = CursorModeType(driver.CursorModeVisible)
CursorModeHidden CursorModeType = CursorModeType(driver.CursorModeHidden)
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 changes: 94 additions & 0 deletions examples/cursor/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
9 changes: 9 additions & 0 deletions internal/driver/cursormode.go → internal/driver/cursor.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,12 @@ const (
CursorModeHidden
CursorModeCaptured
)

type CursorShape int

const (
CursorShapeDefault CursorShape = iota
CursorShapeText
CursorShapeCrosshair
CursorShapePointer
)
3 changes: 3 additions & 0 deletions internal/driver/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ type UI interface {
CursorMode() CursorMode
SetCursorMode(mode CursorMode)

CursorShape() CursorShape
SetCursorShape(shape CursorShape)

IsFullscreen() bool
SetFullscreen(fullscreen bool)

Expand Down
10 changes: 10 additions & 0 deletions internal/glfw/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type (
ModifierKey int
MouseButton int
PeripheralEvent int
StandardCursor int
)

const (
Expand Down Expand Up @@ -142,3 +143,12 @@ func (e ErrorCode) String() string {
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)
)
17 changes: 17 additions & 0 deletions internal/glfw/glfw_notwindows.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ func (w windows) get(win *glfw.Window) *Window {
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 {
m *glfw.Monitor
}
Expand Down Expand Up @@ -163,6 +172,14 @@ func (w *Window) SetCharModsCallback(cbfun CharModsCallback) (previous CharModsC
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) {
var gcb glfw.FramebufferSizeCallback
if cbfun != nil {
Expand Down
18 changes: 18 additions & 0 deletions internal/glfw/glfw_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ func (w glfwWindows) get(win uintptr) *Window {
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 {
m uintptr
}
Expand Down Expand Up @@ -192,6 +202,14 @@ func (w *Window) SetCharModsCallback(cbfun CharModsCallback) (previous CharModsC
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) {
var gcb uintptr
if cbfun != nil {
Expand Down
44 changes: 44 additions & 0 deletions internal/uidriver/glfw/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type UserInterface struct {
runnableOnUnfocused bool
vsync bool
iconImages []image.Image
cursorShape driver.CursorShape

// err must be accessed from the main thread.
err error
Expand Down Expand Up @@ -143,10 +144,13 @@ func init() {
cacheMonitors()
}

var glfwSystemCursors = map[driver.CursorShape]*glfw.Cursor{}

func initialize() error {
if err := glfw.Init(); err != nil {
return err
}

glfw.WindowHint(glfw.Visible, glfw.False)
glfw.WindowHint(glfw.ClientAPI, glfw.NoAPI)

Expand All @@ -168,6 +172,12 @@ func initialize() error {
theUI.initFullscreenWidthInDP = int(fromGLFWMonitorPixel(float64(v.Width), 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
}

Expand Down Expand Up @@ -271,6 +281,21 @@ func (u *UserInterface) setInitCursorMode(mode driver.CursorMode) {
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 {
u.m.RLock()
v := u.initWindowDecorated
Expand Down Expand Up @@ -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 {
if !u.isRunning() {
return devicescale.GetAt(u.initMonitor.GetPos())
Expand Down Expand Up @@ -615,6 +658,7 @@ func (u *UserInterface) createWindow() error {
u.window.SetInputMode(glfw.StickyMouseButtonsMode, glfw.True)
u.window.SetInputMode(glfw.StickyKeysMode, glfw.True)
u.window.SetInputMode(glfw.CursorMode, driverCursorModeToGLFWCursorMode(u.getInitCursorMode()))
u.window.SetCursor(glfwSystemCursors[u.getCursorShape()])
u.window.SetTitle(u.title)
// TODO: Set icons

Expand Down
Loading

0 comments on commit d00d0c8

Please sign in to comment.