From d00d0c85561d29ec345f75f3b822af4c0571233e Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sun, 11 Apr 2021 14:21:38 +0900 Subject: [PATCH] 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 --- cursormode.go => cursor.go | 15 +++- examples/cursor/main.go | 94 ++++++++++++++++++++ internal/driver/{cursormode.go => cursor.go} | 9 ++ internal/driver/ui.go | 3 + internal/glfw/const.go | 10 +++ internal/glfw/glfw_notwindows.go | 17 ++++ internal/glfw/glfw_windows.go | 18 ++++ internal/uidriver/glfw/ui.go | 44 +++++++++ internal/uidriver/js/ui_js.go | 59 ++++++++++-- internal/uidriver/mobile/ui.go | 8 ++ run.go | 8 ++ 11 files changed, 274 insertions(+), 11 deletions(-) rename cursormode.go => cursor.go (63%) create mode 100644 examples/cursor/main.go rename internal/driver/{cursormode.go => cursor.go} (84%) diff --git a/cursormode.go b/cursor.go similarity index 63% rename from cursormode.go rename to cursor.go index f08914936..94e074d00 100644 --- a/cursormode.go +++ b/cursor.go @@ -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) +) diff --git a/examples/cursor/main.go b/examples/cursor/main.go new file mode 100644 index 000000000..f94123718 --- /dev/null +++ b/examples/cursor/main.go @@ -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) + } +} diff --git a/internal/driver/cursormode.go b/internal/driver/cursor.go similarity index 84% rename from internal/driver/cursormode.go rename to internal/driver/cursor.go index 2014b9fbd..6076da35b 100644 --- a/internal/driver/cursormode.go +++ b/internal/driver/cursor.go @@ -21,3 +21,12 @@ const ( CursorModeHidden CursorModeCaptured ) + +type CursorShape int + +const ( + CursorShapeDefault CursorShape = iota + CursorShapeText + CursorShapeCrosshair + CursorShapePointer +) diff --git a/internal/driver/ui.go b/internal/driver/ui.go index 9d18dc2e1..9d5c2167f 100644 --- a/internal/driver/ui.go +++ b/internal/driver/ui.go @@ -45,6 +45,9 @@ type UI interface { CursorMode() CursorMode SetCursorMode(mode CursorMode) + CursorShape() CursorShape + SetCursorShape(shape CursorShape) + IsFullscreen() bool SetFullscreen(fullscreen bool) diff --git a/internal/glfw/const.go b/internal/glfw/const.go index 54099915d..82b2d6bdb 100644 --- a/internal/glfw/const.go +++ b/internal/glfw/const.go @@ -30,6 +30,7 @@ type ( ModifierKey int MouseButton int PeripheralEvent int + StandardCursor int ) const ( @@ -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) +) diff --git a/internal/glfw/glfw_notwindows.go b/internal/glfw/glfw_notwindows.go index 1b4a7e1e7..64253bf61 100644 --- a/internal/glfw/glfw_notwindows.go +++ b/internal/glfw/glfw_notwindows.go @@ -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 } @@ -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 { diff --git a/internal/glfw/glfw_windows.go b/internal/glfw/glfw_windows.go index 8cc6b880f..5d9594574 100644 --- a/internal/glfw/glfw_windows.go +++ b/internal/glfw/glfw_windows.go @@ -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 } @@ -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 { diff --git a/internal/uidriver/glfw/ui.go b/internal/uidriver/glfw/ui.go index 081e8944f..dd1e58cea 100644 --- a/internal/uidriver/glfw/ui.go +++ b/internal/uidriver/glfw/ui.go @@ -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 @@ -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) @@ -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 } @@ -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 @@ -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()) @@ -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 diff --git a/internal/uidriver/js/ui_js.go b/internal/uidriver/js/ui_js.go index 2e77ab18f..57c39a71b 100644 --- a/internal/uidriver/js/ui_js.go +++ b/internal/uidriver/js/ui_js.go @@ -31,11 +31,27 @@ var ( 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 { runnableOnUnfocused bool vsync bool running bool initFocused bool + cursorHidden bool + cursorShape driver.CursorShape sizeChanged bool contextLost bool @@ -107,10 +123,10 @@ func (u *UserInterface) CursorMode() driver.CursorMode { return driver.CursorModeHidden } - if jsutil.Equal(canvas.Get("style").Get("cursor"), stringNone) { - return driver.CursorModeVisible + if u.cursorHidden { + return driver.CursorModeHidden } - return driver.CursorModeHidden + return driver.CursorModeVisible } func (u *UserInterface) SetCursorMode(mode driver.CursorMode) { @@ -118,20 +134,45 @@ func (u *UserInterface) SetCursorMode(mode driver.CursorMode) { return } - var visible bool switch mode { case driver.CursorModeVisible: - visible = true + if u.cursorHidden { + return + } + u.cursorHidden = false case driver.CursorModeHidden: - visible = false + if !u.cursorHidden { + return + } + u.cursorHidden = true default: return } - if visible { - canvas.Get("style").Set("cursor", "auto") + if u.cursorHidden { + canvas.Get("style").Set("cursor", stringNone) } 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)) } } diff --git a/internal/uidriver/mobile/ui.go b/internal/uidriver/mobile/ui.go index 468831721..e6cdaa8a6 100644 --- a/internal/uidriver/mobile/ui.go +++ b/internal/uidriver/mobile/ui.go @@ -370,6 +370,14 @@ func (u *UserInterface) SetCursorMode(mode driver.CursorMode) { // 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 { return false } diff --git a/run.go b/run.go index ce0d77dd1..e595cad99 100644 --- a/run.go +++ b/run.go @@ -241,6 +241,14 @@ func SetCursorMode(mode CursorModeType) { 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 always returns false on browsers or mobiles.