packages feed

nano-ui-sdl-0.1.0.0: lib/NanoUI/Sdl/Cursor.hs

module NanoUI.Sdl.Cursor
  ( SdlCursors (..)
  , initCursors
  , destroyCursors
  , syncPointerCursor
  ) where

import Control.Monad (void, when)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Foreign.Ptr (Ptr, nullPtr)
import NanoUI (Input (..))
import NanoUI.Testing (Context, UiCursorKind (..), uiCursorKind)
import System.Environment (lookupEnv)
import System.IO (hPutStrLn, stderr)
import qualified SDL3.Sys.Bindgen.Mouse as Mouse
import SDL3.Sys.Mouse
  ( createSystemCursorSafe
  , destroyCursorSafe
  , getDefaultCursorSafe
  , setCursorSafe
  )

data SdlCursors = SdlCursors
  { scDefault :: Ptr Mouse.SDL_Cursor
  , scPointer :: Ptr Mouse.SDL_Cursor
  , scText :: Ptr Mouse.SDL_Cursor
  , scMoveFallback :: Ptr Mouse.SDL_Cursor
  , scGrab :: Ptr Mouse.SDL_Cursor
  , scGrabbing :: Ptr Mouse.SDL_Cursor
  , scNsResize :: Ptr Mouse.SDL_Cursor
  , scEwResize :: Ptr Mouse.SDL_Cursor
  , scNwseResize :: Ptr Mouse.SDL_Cursor
  , scNeswResize :: Ptr Mouse.SDL_Cursor
  , scCurrent :: IORef UiCursorKind
  , scTrace :: Bool
  }

initCursors :: IO SdlCursors
initCursors = do
  def <- getDefaultCursorSafe
  ptr <- createSystemCursorSafe Mouse.SDL_SYSTEM_CURSOR_POINTER
  text <- createSystemCursorSafe Mouse.SDL_SYSTEM_CURSOR_TEXT
  moveFallback <- createSystemCursorSafe Mouse.SDL_SYSTEM_CURSOR_MOVE
  ns <- createSystemCursorSafe Mouse.SDL_SYSTEM_CURSOR_NS_RESIZE
  ew <- createSystemCursorSafe Mouse.SDL_SYSTEM_CURSOR_EW_RESIZE
  nwse <- createSystemCursorSafe Mouse.SDL_SYSTEM_CURSOR_NWSE_RESIZE
  nesw <- createSystemCursorSafe Mouse.SDL_SYSTEM_CURSOR_NESW_RESIZE
  -- SDL_SYSTEM_CURSOR_GRAB (27) and GRABBING (28) have no bindgen patterns.
  -- Where SDL or the platform lacks them creation returns NULL, and the move
  -- cursor stands in.
  grab <- createSystemCursorSafe (Mouse.SDL_SystemCursor 27)
  grabbing <- createSystemCursorSafe (Mouse.SDL_SystemCursor 28)
  current <- newIORef UiCursorDefault
  -- Debug aid, read once here so cursor changes stay allocation-free:
  -- NANO_CURSOR_TRACE=1 logs every cursor change to stderr.
  trace <- (== Just "1") <$> lookupEnv "NANO_CURSOR_TRACE"
  -- NULL cursors are tolerated: SDL_SetCursor(NULL) selects the platform
  -- default arrow, which keeps us running on headless/dummy video drivers
  -- where system cursor shapes are unavailable.
  pure
    SdlCursors
      { scDefault = def
      , scPointer = ptr
      , scText = text
      , scMoveFallback = moveFallback
      , scGrab = if grab == nullPtr then moveFallback else grab
      , scGrabbing = if grabbing == nullPtr then moveFallback else grabbing
      , scNsResize = ns
      , scEwResize = ew
      , scNwseResize = nwse
      , scNeswResize = nesw
      , scCurrent = current
      , scTrace = trace
      }

destroyOwnedCursor :: Ptr Mouse.SDL_Cursor -> Ptr Mouse.SDL_Cursor -> IO ()
destroyOwnedCursor cur shared =
  when (cur /= nullPtr && cur /= shared) $
    destroyCursorSafe cur

destroyCursors :: SdlCursors -> IO ()
destroyCursors cursors = do
  let fb = scMoveFallback cursors
  destroyOwnedCursor (scPointer cursors) nullPtr
  destroyOwnedCursor (scText cursors) nullPtr
  destroyOwnedCursor (scNsResize cursors) nullPtr
  destroyOwnedCursor (scEwResize cursors) nullPtr
  destroyOwnedCursor (scNwseResize cursors) nullPtr
  destroyOwnedCursor (scNeswResize cursors) nullPtr
  destroyOwnedCursor (scGrab cursors) fb
  destroyOwnedCursor (scGrabbing cursors) fb
  destroyCursorSafe fb

cursorPtr :: SdlCursors -> UiCursorKind -> Ptr Mouse.SDL_Cursor
cursorPtr cursors = \case
  UiCursorDefault -> scDefault cursors
  UiCursorPointer -> scPointer cursors
  UiCursorText -> scText cursors
  UiCursorGrab -> scGrab cursors
  UiCursorGrabbing -> scGrabbing cursors
  UiCursorNsResize -> scNsResize cursors
  UiCursorEwResize -> scEwResize cursors
  UiCursorNwseResize -> scNwseResize cursors
  UiCursorNeswResize -> scNeswResize cursors

syncPointerCursor :: SdlCursors -> Context -> Input -> IO ()
syncPointerCursor cursors ctx inp = do
  want <- uiCursorKind ctx inp
  cur <- readIORef (scCurrent cursors)
  when (want /= cur) $ do
    when (scTrace cursors) $
      hPutStrLn stderr ("cursor: " ++ show want ++ " at " ++ show (inputMousePos inp))
    void $ setCursorSafe (cursorPtr cursors want)
    writeIORef (scCurrent cursors) want