Fancy Mouse Cursors

The gamepad mouse cursor has become THE mouse cursor. It is always visible and your
real cursor is hidden, and this way the game can swap out other cursors for certain
scenarios:

* The Pencil Tool in the editor will use a pencil cursor over the level canvas.
* The Flood Tool has a custom Flood cursor so you don't forget it's selected!

Other improvements:

* The Palette buttons in the editor now render using their swatch's pattern
  instead of only using its color.
* If you have an ultra HD monitor and open a Bounded level in the editor which
  is too small to fill your screen, the editor canvas limits its size to fit
  the level (preferable over showing parts of the level you can't actually play
  as it's out of bounds).
* The "brush size" box is only drawn around the cursor when a relevant tool is
  selected (Pencil, Line, Rect, Ellipse, Eraser)
This commit is contained in:
Noah 2022-05-04 22:38:26 -07:00
parent 9b75f1b039
commit 4efa8d00fc
11 changed files with 176 additions and 23 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
assets/sprites/pencil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@ -199,6 +199,10 @@ func main() {
game := doodle.New(c.Bool("debug"), engine) game := doodle.New(c.Bool("debug"), engine)
game.SetupEngine() game.SetupEngine()
// Hide the mouse cursor over the window, we draw our own
// sprite image for it.
engine.ShowCursor(false)
// Set the app window icon. // Set the app window icon.
if engine, ok := game.Engine.(*sdl.Renderer); ok { if engine, ok := game.Engine.(*sdl.Renderer); ok {
if icon, err := sprites.LoadImage(game.Engine, balance.WindowIcon); err == nil { if icon, err := sprites.LoadImage(game.Engine, balance.WindowIcon); err == nil {

View File

@ -14,7 +14,11 @@ var (
GoldCoin = "assets/sprites/gold.png" GoldCoin = "assets/sprites/gold.png"
SilverCoin = "assets/sprites/silver.png" SilverCoin = "assets/sprites/silver.png"
LockIcon = "assets/sprites/padlock.png" LockIcon = "assets/sprites/padlock.png"
CursorIcon = "assets/sprites/pointer.png"
// Cursors
CursorIcon = "assets/sprites/pointer.png"
PencilIcon = "assets/sprites/pencil.png"
FloodCursor = "assets/sprites/flood-cursor.png"
// Title Screen Font // Title Screen Font
TitleScreenFont = render.Text{ TitleScreenFont = render.Text{

86
pkg/cursor/cursor.go Normal file
View File

@ -0,0 +1,86 @@
// Package cursor handles custom mouse cursor sprite images.
package cursor
import (
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/apps/doodle/pkg/sprites"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
)
type Cursor struct {
Filename string
Sprite *ui.Image
Offset render.Point
}
// Current selected cursor to draw on screen.
var Current *Cursor
// NoCursor hides the cursor entirely.
var NoCursor = &Cursor{}
// Draw the cursor on screen.
func Draw(e render.Engine) {
if Current == nil {
Current = NewPointer(e)
}
if Current.Sprite != nil {
Current.Sprite.Present(e, shmem.Cursor)
}
}
// NewPointer initializes the default pointer cursor.
func NewPointer(e render.Engine) *Cursor {
if pointer != nil {
return pointer
}
img, err := sprites.LoadImage(e, balance.CursorIcon)
if err != nil {
log.Error("NewPointer: %s", err)
}
return &Cursor{
Filename: balance.CursorIcon,
Sprite: img,
}
}
// NewPencil initializes the pencil cursor.
func NewPencil(e render.Engine) *Cursor {
if pencil != nil {
return pencil
}
img, err := sprites.LoadImage(e, balance.PencilIcon)
if err != nil {
log.Error("NewPencil: %s", err)
}
return &Cursor{
Filename: balance.PencilIcon,
Sprite: img,
}
}
// NewFlood initializes the Flood cursor.
func NewFlood(e render.Engine) *Cursor {
if pencil != nil {
return pencil
}
img, err := sprites.LoadImage(e, balance.FloodCursor)
if err != nil {
log.Error("NewFlood: %s", err)
}
return &Cursor{
Filename: balance.FloodCursor,
Sprite: img,
}
}
// Cached singletons of the cursors.
var (
pointer *Cursor
pencil *Cursor
flood *Cursor
)

View File

@ -9,6 +9,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/branding" "git.kirsle.net/apps/doodle/pkg/branding"
"git.kirsle.net/apps/doodle/pkg/cursor"
"git.kirsle.net/apps/doodle/pkg/enum" "git.kirsle.net/apps/doodle/pkg/enum"
"git.kirsle.net/apps/doodle/pkg/gamepad" "git.kirsle.net/apps/doodle/pkg/gamepad"
"git.kirsle.net/apps/doodle/pkg/keybind" "git.kirsle.net/apps/doodle/pkg/keybind"
@ -209,8 +210,8 @@ func (d *Doodle) Run() error {
// Draw the debug overlay over all scenes. // Draw the debug overlay over all scenes.
d.DrawDebugOverlay() d.DrawDebugOverlay()
// Let the gamepad controller draw in case of MouseMode to show the cursor. // Draw our custom mouse cursor to the screen.
gamepad.Draw(d.Engine) cursor.Draw(d.Engine)
// Render the pixels to the screen. // Render the pixels to the screen.
err = d.Engine.Present() err = d.Engine.Present()

View File

@ -8,6 +8,7 @@ import (
"time" "time"
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/cursor"
"git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/drawtool" "git.kirsle.net/apps/doodle/pkg/drawtool"
"git.kirsle.net/apps/doodle/pkg/enum" "git.kirsle.net/apps/doodle/pkg/enum"
@ -609,5 +610,8 @@ func (s *EditorScene) Destroy() error {
// their bitmaps cached and will regen the textures as needed. // their bitmaps cached and will regen the textures as needed.
s.UI.Teardown() s.UI.Teardown()
// Reset the cursor to default.
cursor.Current = cursor.NewPointer(s.d.Engine)
return nil return nil
} }

View File

@ -395,6 +395,7 @@ func (u *EditorUI) SetupWorkspace(d *Doodle) *ui.Frame {
func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas { func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas {
drawing := uix.NewCanvas(balance.ChunkSize, true) drawing := uix.NewCanvas(balance.ChunkSize, true)
drawing.Name = "edit-canvas" drawing.Name = "edit-canvas"
drawing.FancyCursors = true
drawing.Palette = level.DefaultPalette() drawing.Palette = level.DefaultPalette()
drawing.SetBackground(render.White) drawing.SetBackground(render.White)
if len(drawing.Palette.Swatches) > 0 { if len(drawing.Palette.Swatches) > 0 {
@ -517,7 +518,22 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas {
// _wanted_ to be smaller, as in Doodad Editing Mode. // _wanted_ to be smaller, as in Doodad Editing Mode.
func (u *EditorUI) ExpandCanvas(e render.Engine) { func (u *EditorUI) ExpandCanvas(e render.Engine) {
if u.Scene.DrawingType == enum.LevelDrawing { if u.Scene.DrawingType == enum.LevelDrawing {
u.Canvas.Resize(u.Workspace.Size()) var (
workspaceSize = u.Workspace.Size()
maxSize = workspaceSize
)
// If the level is bounded make that the max canvas size.
if u.Scene.Level != nil && u.Scene.Level.PageType >= level.Bounded {
if u.Scene.Level.MaxWidth < int64(maxSize.W) {
maxSize.W = int(u.Scene.Level.MaxWidth)
}
if u.Scene.Level.MaxHeight < int64(maxSize.H) {
maxSize.H = int(u.Scene.Level.MaxHeight)
}
}
u.Canvas.Resize(maxSize)
} else { } else {
// Size is managed externally. // Size is managed externally.
} }

View File

@ -4,8 +4,11 @@ import (
"fmt" "fmt"
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/uix"
"git.kirsle.net/apps/doodle/pkg/usercfg" "git.kirsle.net/apps/doodle/pkg/usercfg"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui" "git.kirsle.net/go/ui"
) )
@ -94,12 +97,15 @@ func (u *EditorUI) setupPaletteFrame(window *ui.Window) *ui.Frame {
frame.Pack(row, packConfig) frame.Pack(row, packConfig)
} }
colorbox := ui.NewFrame(swatch.Name) // Fancy colorbox: show the color AND the texture of each swatch.
colorbox.Configure(ui.Config{ var (
Width: width, colorbox = uix.NewCanvas(width, false)
Height: width, chunker = level.NewChunker(width)
Background: swatch.Color, size = render.NewRect(width, width)
}) )
chunker.SetRect(size, swatch)
colorbox.Resize(size)
colorbox.Load(u.Canvas.Palette, chunker)
btn := ui.NewRadioButton("palette", &u.selectedSwatch, swatch.Name, colorbox) btn := ui.NewRadioButton("palette", &u.selectedSwatch, swatch.Name, colorbox)
btn.Configure(ui.Config{ btn.Configure(ui.Config{

View File

@ -7,6 +7,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/collision" "git.kirsle.net/apps/doodle/pkg/collision"
"git.kirsle.net/apps/doodle/pkg/cursor"
"git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/drawtool" "git.kirsle.net/apps/doodle/pkg/drawtool"
"git.kirsle.net/apps/doodle/pkg/filesystem" "git.kirsle.net/apps/doodle/pkg/filesystem"
@ -56,6 +57,10 @@ type Canvas struct {
// NoLimitScroll suppresses the scroll limit for bounded levels. // NoLimitScroll suppresses the scroll limit for bounded levels.
NoLimitScroll bool NoLimitScroll bool
// Show custom mouse cursors over this canvas (eg. editor tools)
FancyCursors bool
cursor *cursor.Cursor
// Underlying chunk data for the drawing. // Underlying chunk data for the drawing.
level *level.Level level *level.Level
chunks *level.Chunker chunks *level.Chunker

View File

@ -1,6 +1,8 @@
package uix package uix
import ( import (
"git.kirsle.net/apps/doodle/pkg/cursor"
"git.kirsle.net/apps/doodle/pkg/drawtool"
"git.kirsle.net/apps/doodle/pkg/shmem" "git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"git.kirsle.net/go/ui" "git.kirsle.net/go/ui"
@ -27,24 +29,49 @@ func (w *Canvas) IsCursorOver() bool {
// brush size, and draws a "preview rect" under the cursor of how big a click // brush size, and draws a "preview rect" under the cursor of how big a click
// will be at that size. // will be at that size.
func (w *Canvas) presentCursor(e render.Engine) { func (w *Canvas) presentCursor(e render.Engine) {
// Are we to show a custom mouse cursor?
if w.FancyCursors {
switch w.Tool {
case drawtool.PencilTool:
w.cursor = cursor.NewPencil(e)
case drawtool.FloodTool:
w.cursor = cursor.NewFlood(e)
default:
w.cursor = nil
}
if w.IsCursorOver() && w.cursor != nil {
cursor.Current = w.cursor
} else {
cursor.Current = cursor.NewPointer(e)
}
}
if !w.IsCursorOver() { if !w.IsCursorOver() {
return return
} }
// Are we editing with a thick brush? // Are we editing with a thick brush?
if w.BrushSize > 0 { if w.Tool == drawtool.LineTool || w.Tool == drawtool.RectTool ||
var r = w.BrushSize w.Tool == drawtool.PencilTool || w.Tool == drawtool.EllipseTool ||
rect := render.Rect{ w.Tool == drawtool.EraserTool {
X: shmem.Cursor.X - r,
Y: shmem.Cursor.Y - r, // Draw a box where the brush size is.
W: r * 2, if w.BrushSize > 0 {
H: r * 2, var r = w.BrushSize
rect := render.Rect{
X: shmem.Cursor.X - r,
Y: shmem.Cursor.Y - r,
W: r * 2,
H: r * 2,
}
e.DrawRect(render.Black, rect)
rect.X++
rect.Y++
rect.W -= 2
rect.H -= 2
e.DrawRect(render.RGBA(153, 153, 153, 153), rect)
} }
e.DrawRect(render.Black, rect)
rect.X++
rect.Y++
rect.W -= 2
rect.H -= 2
e.DrawRect(render.RGBA(153, 153, 153, 153), rect)
} }
} }