packages feed

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

{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE NoFieldSelectors #-}

-- | SDL3 event polling and waiting, and translation of SDL events into
-- 'NanoUI.Input.Input'.
module NanoUI.Sdl.Input
  ( SdlEvent (..)
  , pollEvents
  , waitEvent
  , waitEventTimeout
  , applyEvent
  , isHardQuit
  , isButtonEdge
  ) where

import Data.Bits ((.&.))
import Data.IORef (readIORef)
import qualified Data.Text as T
import Data.Text (Text)
import qualified Data.Text.Foreign as TF
import Data.Word (Word32)
import Foreign.C.Types (CFloat)
import Foreign.Marshal.Alloc (alloca)
import Foreign.Ptr (Ptr, nullPtr)
import Foreign.Storable (Storable (..))
import GHC.Records.Compat (getField)
import SDL3.Sys.Bindgen.Runtime.CBool qualified as CBool
import SDL3.Sys.Bindgen.Runtime.PtrConst qualified as PtrConst
import NanoUI
  ( Input (..)
  , Key (..)
  , Modifiers (..)
  , DropEvent (..)
  , DropType (..)
  , V2 (..)
  , appendInputKey
  , v2Add
  )
import NanoUI.Input (MouseButton (..), appendDropEvent, applyMouseButton)
import NanoUI.Sdl.Display (refreshEventType)
import SDL3.Sys.Bindgen.Events
  ( SDL_Event (..)
  , SDL_EventType (..)
  , SDL_KeyboardEvent
  )
import SDL3.Sys.Bindgen.Events qualified as Events
import SDL3.Sys.Events (pollEventSafe, waitEventSafe, waitEventTimeoutSafe)
import SDL3.Sys.Bindgen.Keycode
  ( SDL_Keycode (..)
  , SDL_Keymod (..)
  , sDLK_BACKSPACE
  , sDLK_DELETE
  , sDLK_DOWN
  , sDLK_END
  , sDLK_ESCAPE
  , sDLK_HOME
  , sDLK_LEFT
  , sDLK_RETURN
  , sDLK_RIGHT
  , sDLK_TAB
  , sDLK_UP
  , sDL_KMOD_ALT
  , sDL_KMOD_CTRL
  , sDL_KMOD_SHIFT
  )
import SDL3.Sys.Bindgen.Mouse (sDL_BUTTON_LEFT, sDL_BUTTON_RIGHT)
import SDL3.Sys.Bindgen.Stdinc (Sint32 (..), Uint32 (..))
import SDL3.Sys.Keyboard (getModState)

data SdlEvent
  = EvQuit
  | EvResize Int Int
  | EvDisplayScale
  | EvKey Key Modifiers
  | EvText Text Modifiers
  | EvMouseMotion V2 Modifiers
  | EvMousePress V2 Modifiers
  | EvMouseRelease V2 Modifiers
  | EvMouseRightPress V2 Modifiers
  | EvMouseRightRelease V2 Modifiers
  | EvScroll V2
  | EvDrop DropEvent
  | EvRefresh
  | EvWindowRedraw
  deriving (Eq, Show)

-- | Drain every pending event, oldest first.
pollEvents :: IO [SdlEvent]
pollEvents =
  alloca $ \(p :: Ptr SDL_Event) -> do
    refreshTy <- readIORef refreshEventType
    let drain acc = do
          got <- pollEventSafe p
          if got
            then decodeEvent refreshTy p >>= \ev -> drain (maybe acc (: acc) ev)
            else pure (reverse acc)
    drain []

waitEvent :: IO (Maybe SdlEvent)
waitEvent =
  alloca $ \p -> do
    got <- waitEventSafe p
    if got then readIORef refreshEventType >>= \ty -> decodeEvent ty p else pure Nothing

waitEventTimeout :: Int -> IO (Maybe SdlEvent)
waitEventTimeout ms =
  alloca $ \p -> do
    got <- waitEventTimeoutSafe p (fromIntegral ms)
    if got then readIORef refreshEventType >>= \ty -> decodeEvent ty p else pure Nothing

-- | Translate one SDL event, given the refresh event type; 'Nothing' for
-- events the UI ignores.
decodeEvent :: Word32 -> Ptr SDL_Event -> IO (Maybe SdlEvent)
decodeEvent refreshTy p = do
  Uint32 w <- peek p.type'
  if refreshTy /= 0 && w == refreshTy
    then pure (Just EvRefresh)
    else case SDL_EventType (fromIntegral w) of
      Events.SDL_EVENT_QUIT -> pure (Just EvQuit)
      Events.SDL_EVENT_WINDOW_RESIZED -> Just <$> windowResized p
      -- Pixel size changes are ignored here; syncDisplay re-queries logical size.
      Events.SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED -> pure (Just EvDisplayScale)
      Events.SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED -> pure (Just EvDisplayScale)
      -- The window manager damaged our window surface (occlusion, compositor
      -- effects, restore). The backbuffer contents are gone; the next present
      -- must be full or stale regions flash.
      Events.SDL_EVENT_WINDOW_EXPOSED -> pure (Just EvWindowRedraw)
      Events.SDL_EVENT_WINDOW_RESTORED -> pure (Just EvWindowRedraw)
      Events.SDL_EVENT_KEY_DOWN -> keyDown p
      Events.SDL_EVENT_TEXT_INPUT -> textInput p
      Events.SDL_EVENT_MOUSE_MOTION -> Just <$> mouseMotion p
      Events.SDL_EVENT_MOUSE_BUTTON_DOWN -> mouseButton p True
      Events.SDL_EVENT_MOUSE_BUTTON_UP -> mouseButton p False
      Events.SDL_EVENT_MOUSE_WHEEL -> Just <$> mouseWheel p
      Events.SDL_EVENT_DROP_FILE -> Just <$> dropEvent p DropFile
      Events.SDL_EVENT_DROP_TEXT -> Just <$> dropEvent p DropText
      Events.SDL_EVENT_DROP_BEGIN -> Just <$> dropEvent p DropBegin
      Events.SDL_EVENT_DROP_COMPLETE -> Just <$> dropEvent p DropComplete
      Events.SDL_EVENT_DROP_POSITION -> Just <$> dropEvent p DropPosition
      _ -> pure Nothing

keyDown :: Ptr SDL_Event -> IO (Maybe SdlEvent)
keyDown p = do
  ke <- peek p.key
  let mods = keyModifiers ke
      code = fromIntegral (getField @"key" ke :: SDL_Keycode) :: Word32
      repeating = CBool.toBool (getField @"repeat" ke)
  pure $
    case mapSpecialKey code of
      Just k
        | not repeating || isRepeatableKey k -> Just (EvKey k mods)
        | otherwise -> Nothing
      Nothing
        -- Ctrl chords produce no text-input event; report the printable key
        -- symbol (SDL folds Shift into it, so Ctrl+Shift+= arrives as '+').
        | modCtrl mods && code >= 32 && code <= 126 ->
            Just (EvText (T.singleton (toEnum (fromIntegral code))) mods)
        | otherwise -> Nothing

textInput :: Ptr SDL_Event -> IO (Maybe SdlEvent)
textInput p = do
  te <- peek p.text
  mods <- peekModifiers
  let textPtr = PtrConst.unsafeToPtr (getField @"text" te)
  if textPtr == nullPtr
    then pure Nothing
    else do
      txt <- TF.peekCString textPtr
      pure (if T.null txt then Nothing else Just (EvText txt mods))

mouseMotion :: Ptr SDL_Event -> IO SdlEvent
mouseMotion p = do
  me <- peek p.motion
  mods <- peekModifiers
  let x = getField @"x" me :: CFloat
      y = getField @"y" me :: CFloat
  pure (EvMouseMotion (V2 (realToFrac x) (realToFrac y)) mods)

mouseButton :: Ptr SDL_Event -> Bool -> IO (Maybe SdlEvent)
mouseButton p down = do
  be <- peek p.button
  mods <- peekModifiers
  let x = getField @"x" be :: CFloat
      y = getField @"y" be :: CFloat
      pos = V2 (realToFrac x) (realToFrac y)
      btn = getField @"button" be
  pure $
    if btn == fromIntegral sDL_BUTTON_LEFT
      then Just (if down then EvMousePress pos mods else EvMouseRelease pos mods)
      else
        if btn == fromIntegral sDL_BUTTON_RIGHT
          then Just (if down then EvMouseRightPress pos mods else EvMouseRightRelease pos mods)
          else Nothing

mouseWheel :: Ptr SDL_Event -> IO SdlEvent
mouseWheel p = do
  we <- peek p.wheel
  let x = getField @"x" we :: CFloat
      y = getField @"y" we :: CFloat
  pure (EvScroll (V2 (realToFrac x) (negate (realToFrac y))))

windowResized :: Ptr SDL_Event -> IO SdlEvent
windowResized p = do
  we <- peek p.window
  let Sint32 w = getField @"data1" we
      Sint32 h = getField @"data2" we
  pure (EvResize (fromIntegral w) (fromIntegral h))

dropEvent :: Ptr SDL_Event -> DropType -> IO SdlEvent
dropEvent p ty = do
  de <- peek p.drop
  let x = getField @"x" de :: CFloat
      y = getField @"y" de :: CFloat
      at = Just (V2 (realToFrac x) (realToFrac y))
      pos =
        case ty of
          DropPosition -> at
          DropFile -> at
          DropText -> at
          _ -> Nothing
      dataPtr = PtrConst.unsafeToPtr (getField @"data'" de)
  payload <-
    if dataPtr == nullPtr
      then pure ""
      else TF.peekCString dataPtr
  pure (EvDrop (DropEvent ty pos payload))

peekModifiers :: IO Modifiers
peekModifiers = modFromKeymod <$> getModState

modFromKeymod :: SDL_Keymod -> Modifiers
modFromKeymod km =
  let m = word32 km
   in Modifiers
        { modShift = m .&. word32 sDL_KMOD_SHIFT /= 0
        , modCtrl = m .&. word32 sDL_KMOD_CTRL /= 0
        , modAlt = m .&. word32 sDL_KMOD_ALT /= 0
        }

keyModifiers :: SDL_KeyboardEvent -> Modifiers
keyModifiers ke = modFromKeymod (getField @"mod" ke)

word32 :: Integral a => a -> Word32
word32 = fromIntegral

mapSpecialKey :: Word32 -> Maybe Key
mapSpecialKey k
  | k == word32 sDLK_ESCAPE = Just KeyEscape
  | k == word32 sDLK_RETURN = Just KeyEnter
  | k == word32 sDLK_TAB = Just KeyTab
  | k == word32 sDLK_BACKSPACE = Just KeyBackspace
  | k == word32 sDLK_DELETE = Just KeyDelete
  | k == word32 sDLK_LEFT = Just KeyLeft
  | k == word32 sDLK_RIGHT = Just KeyRight
  | k == word32 sDLK_UP = Just KeyUp
  | k == word32 sDLK_DOWN = Just KeyDown
  | k == word32 sDLK_HOME = Just KeyHome
  | k == word32 sDLK_END = Just KeyEnd
  | otherwise = Nothing

isRepeatableKey :: Key -> Bool
isRepeatableKey KeyBackspace = True
isRepeatableKey KeyDelete = True
isRepeatableKey KeyLeft = True
isRepeatableKey KeyRight = True
isRepeatableKey KeyUp = True
isRepeatableKey KeyDown = True
isRepeatableKey KeyHome = True
isRepeatableKey KeyEnd = True
isRepeatableKey _ = False

applyEvent :: Input -> SdlEvent -> Input
applyEvent inp ev =
  case ev of
    EvQuit -> inp
    EvDisplayScale -> inp
    EvResize _ _ -> inp
    EvKey k mods -> inp {inputKeys = appendInputKey k (inputKeys inp), inputModifiers = mods}
    EvText txt mods ->
      inp {inputChars = inputChars inp <> txt, inputModifiers = mods}
    EvMouseMotion pos mods ->
      inp {inputMousePos = pos, inputModifiers = mods}
    EvMousePress pos mods ->
      (applyMouseButton MouseLeft True inp) {inputMousePos = pos, inputModifiers = mods}
    EvMouseRelease pos mods ->
      (applyMouseButton MouseLeft False inp) {inputMousePos = pos, inputModifiers = mods}
    EvMouseRightPress pos mods ->
      (applyMouseButton MouseRight True inp) {inputMousePos = pos, inputModifiers = mods}
    EvMouseRightRelease pos mods ->
      (applyMouseButton MouseRight False inp) {inputMousePos = pos, inputModifiers = mods}
    EvScroll delta -> inp {inputScroll = v2Add (inputScroll inp) delta}
    EvDrop dropEv -> inp {inputDrops = appendDropEvent dropEv (inputDrops inp)}
    EvRefresh -> inp {inputWindowRedraw = True}
    EvWindowRedraw -> inp {inputWindowRedraw = True}

isButtonEdge :: SdlEvent -> Bool
isButtonEdge ev =
  case ev of
    EvMousePress _ _ -> True
    EvMouseRelease _ _ -> True
    EvMouseRightPress _ _ -> True
    EvMouseRightRelease _ _ -> True
    _ -> False

isHardQuit :: SdlEvent -> Bool
isHardQuit ev =
  case ev of
    EvText txt mods -> (txt == "c" && modCtrl mods) || txt == "\ETX"
    _ -> False