aztecs-sdl-0.6.0: src/Aztecs/SDL.hs
{-# LANGUAGE Arrows #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TypeApplications #-}
-- |
-- Module : Aztecs.SDL
-- Copyright : (c) Matt Hunzinger, 2025
-- License : BSD-style (see the LICENSE file in the distribution)
--
-- Maintainer : matt@hunzinger.me
-- Stability : provisional
-- Portability : non-portable (GHC extensions)
module Aztecs.SDL
( -- * Window components
WindowRenderer (..),
-- * Camera components
Camera (..),
CameraTarget (..),
-- * Surface components
Surface (..),
SurfaceTarget (..),
SurfaceTexture (..),
-- * Input
-- ** Keyboard input
fromSDLKeycode,
toSDLKeycode,
-- ** Mouse input
mouseButtonFromSDL,
mouseButtonToSDL,
motionFromSDL,
motionToSDL,
-- * Systems
setup,
update,
draw,
-- ** Primitive systems
addWindows,
buildTextures,
addSurfaceTargets,
handleInput,
updateTime,
)
where
import Aztecs
import Aztecs.Camera
import qualified Aztecs.ECS.Access as A
import qualified Aztecs.ECS.Query as Q
import qualified Aztecs.ECS.System as S
import Aztecs.Input
import Control.Arrow
import Control.DeepSeq
import Control.Monad
import Control.Monad.IO.Class
import Data.Foldable
import qualified Data.Map.Strict as Map
import Data.Maybe
import qualified Data.Text as T
import GHC.Generics
import SDL hiding (InputMotion (..), MouseButton (..), Surface, Texture, Window, windowTitle)
import qualified SDL
-- | Window renderer component.
--
-- @since 0.5
data WindowRenderer = WindowRenderer
{ -- | SDL window.
--
-- @since 0.5
windowRendererRaw :: !SDL.Window,
-- | SDL renderer.
--
-- @since 0.5
windowRenderer :: !Renderer
}
deriving (Show, Generic)
-- | @since 0.5
instance Component WindowRenderer
-- | @since 0.5
instance NFData WindowRenderer where
rnf = rwhnf
-- | Setup SDL
--
-- @since 0.5
setup :: (MonadAccess b m, MonadIO m) => m ()
setup = do
liftIO initializeAll
A.spawn_ . bundle $ Time 0
A.spawn_ $ bundle keyboardInput
A.spawn_ $ bundle mouseInput
-- | Update SDL windows
--
-- @since 0.5
update ::
( ArrowQueryReader qr,
ArrowDynamicQueryReader qr,
ArrowQuery m q,
MonadSystem q s,
MonadReaderSystem qr s,
MonadIO s,
MonadAccess b s,
MonadIO m
) =>
s ()
update = do
updateTime
join addWindows
join addCameraTargets
join addSurfaceTargets
join buildTextures
handleInput
-- | Setup new windows.
--
-- @since 0.5
addWindows ::
( ArrowQueryReader q,
ArrowDynamicQueryReader q,
MonadReaderSystem q s,
MonadIO s,
MonadAccess b ma
) =>
s (ma ())
addWindows = do
newWindows <- S.filter () (Q.entity &&& Q.fetch @_ @Window) (without @WindowRenderer)
newWindows' <- liftIO $ mapM createWindowRenderer newWindows
return $ mapM_ insertWindowRenderer newWindows'
where
createWindowRenderer (eId, window) = do
sdlWindow <- createWindow (T.pack $ windowTitle window) defaultWindow
renderer <- createRenderer sdlWindow (-1) defaultRenderer
return (eId, sdlWindow, renderer)
insertWindowRenderer (eId, window, renderer) = A.insert eId . bundle $ WindowRenderer window renderer
-- | Surface texture component.
--
-- @since 0.5
newtype SurfaceTexture = SurfaceTexture
{ -- | SDL texture.
--
-- @since 0.5
unSurfaceTexture :: SDL.Texture
}
deriving (Generic)
-- | @since 0.5
instance Component SurfaceTexture
-- | @since 0.5
instance NFData SurfaceTexture where
rnf = rwhnf
-- | Query all window textures.
--
-- @since 0.5
allWindowTextures ::
(ArrowQueryReader q, ArrowDynamicQueryReader q, MonadReaderSystem q s) =>
s [(WindowRenderer, [(EntityID, Surface, Transform2D, Maybe SurfaceTexture)])]
allWindowTextures =
map (second (concatMap snd))
<$> allWindowDraws
(arr (const ()))
( proc () -> do
e <- Q.entity -< ()
surface <- Q.fetch -< ()
transform <- Q.fetch -< ()
texture <- Q.fetchMaybe -< ()
returnA -< (e, surface, transform, texture)
)
-- | Build textures from surfaces in preparation for `drawTextures`.
--
-- @since 0.5
buildTextures ::
( ArrowQueryReader q,
ArrowDynamicQueryReader q,
MonadReaderSystem q s,
MonadAccess b m,
MonadIO m
) =>
s (m ())
buildTextures = do
windowTextures <- allWindowTextures
let go =
mapM_
( \(window, cameraDraws) -> do
let renderer = windowRenderer window
mapM_
( \(eId, surface, transform, maybeTexture) -> do
sdlTexture <- SDL.createTextureFromSurface renderer $ sdlSurface surface
textureDesc <- queryTexture sdlTexture
case maybeTexture of
Just (SurfaceTexture lastTexture) -> destroyTexture lastTexture
Nothing -> return ()
A.insert eId . bundle $ SurfaceTexture sdlTexture
A.insert
eId
( bundle . Size $
transformScale transform
* maybe (fromIntegral <$> V2 (textureWidth textureDesc) (textureHeight textureDesc)) (\(Rectangle _ s) -> fmap fromIntegral s) (surfaceBounds surface)
)
)
cameraDraws
)
windowTextures
return go
-- | Draw all textures to their targetted windows.
--
-- @since 0.5
draw :: (ArrowQueryReader q, ArrowDynamicQueryReader q, MonadReaderSystem q s, MonadIO s) => s ()
draw = do
cameraSurfaces <- allCameraSurfaces
mapM_
( \(window, cameraDraws) -> do
let renderer = windowRenderer window
clear renderer
mapM_
( \((camera, cameraTransform), cameraDraws') -> do
rendererScale renderer $= fmap realToFrac (cameraScale camera)
rendererViewport renderer
$= Just
( Rectangle
(P (fromIntegral <$> transformTranslation cameraTransform))
(fromIntegral <$> cameraViewport camera)
)
mapM_
( \(surface, transform, texture) -> do
textureDesc <- queryTexture $ unSurfaceTexture texture
copyEx
renderer
(unSurfaceTexture texture)
(fmap fromIntegral <$> surfaceBounds surface)
( Just
( Rectangle
(fmap fromIntegral . P $ transformTranslation transform)
( maybe (fromIntegral <$> V2 (textureWidth textureDesc) (textureHeight textureDesc)) (\(Rectangle _ s) -> fmap fromIntegral s) (surfaceBounds surface)
)
)
)
(realToFrac $ transformRotation transform)
Nothing
(V2 False False)
)
cameraDraws'
)
cameraDraws
rendererDrawColor renderer $= V4 0 0 0 255
present renderer
)
cameraSurfaces
-- | Query all cameras and their target surfaces.
--
-- @since 0.5
allCameraSurfaces ::
(ArrowQueryReader q, ArrowDynamicQueryReader q, MonadReaderSystem q s) =>
s [(WindowRenderer, [((Camera, Transform2D), [(Surface, Transform2D, SurfaceTexture)])])]
allCameraSurfaces =
allWindowDraws
(Q.fetch &&& Q.fetch)
( proc () -> do
surface <- Q.fetch -< ()
transform <- Q.fetch -< ()
texture <- Q.fetch -< ()
returnA -< (surface, transform, texture)
)
-- | Query all windows and drawable surfaces.
--
-- @since 0.5
allWindowDraws ::
(ArrowQueryReader q, ArrowDynamicQueryReader q, MonadReaderSystem q s) =>
q () a ->
q () b ->
s [(WindowRenderer, [(a, [b])])]
allWindowDraws qA qB = do
cameras <-
S.all
()
( proc () -> do
eId <- Q.entity -< ()
cameraTarget <- Q.fetch @_ @CameraTarget -< ()
a <- qA -< ()
returnA -< (eId, cameraTarget, a)
)
windows <- S.all () (Q.entity &&& Q.fetch @_ @WindowRenderer)
draws <-
S.all
()
( proc () -> do
t <- Q.fetch @_ @SurfaceTarget -< ()
a <- qB -< ()
returnA -< (t, a)
)
let cameraDraws =
map
( \(eId, cameraTarget, b) ->
( cameraTarget,
b,
mapMaybe
( \(surfaceTarget, a) ->
if drawTargetCamera surfaceTarget == eId
then Just a
else Nothing
)
draws
)
)
cameras
windowDraws =
map
( \(eId, window) ->
( window,
map (\(_, a, b) -> (a, b)) $
filter (\(cameraTarget, _, _) -> cameraTargetWindow cameraTarget == eId) cameraDraws
)
)
windows
return windowDraws
-- | Surface target component.
-- This component can be used to specify which `Camera` to draw a `Surface` to.
--
-- @since 0.5
newtype SurfaceTarget = SurfaceTarget {drawTargetCamera :: EntityID}
deriving (Eq, Show, Generic, NFData)
instance Component SurfaceTarget
-- | Surface component.
-- This component can be used to draw to a window.
--
-- @since 0.5
data Surface = Surface
{ sdlSurface :: !SDL.Surface,
surfaceBounds :: !(Maybe (Rectangle Int))
}
-- | @since 0.5
instance Component Surface
-- | @since 0.5
instance NFData Surface where
rnf = rwhnf
-- | Add `SurfaceTarget` components to entities with a new `Surface` component.
--
-- @since 0.5
addSurfaceTargets ::
( ArrowQueryReader q,
ArrowDynamicQueryReader q,
MonadReaderSystem q s,
MonadAccess b m
) =>
s (m ())
addSurfaceTargets = do
cameras <- S.all () (Q.entity &&& Q.fetch @_ @Camera)
newDraws <- S.filter () (Q.entity &&& Q.fetch @_ @Surface) (without @SurfaceTarget)
let go = case cameras of
(cameraEId, _) : _ -> mapM_ (\(eId, _) -> A.insert eId . bundle $ SurfaceTarget cameraEId) newDraws
_ -> return ()
return go
-- | Update the current `Time`.
--
-- @since 0.5
updateTime ::
( ArrowQuery m q,
MonadSystem q s,
MonadIO m
) =>
s ()
updateTime = void . S.mapSingle () $ Q.adjustM (\_ _ -> Time <$> SDL.ticks)
-- | Keyboard input system.
--
-- @since 0.5
handleInput ::
( ArrowQueryReader qr,
MonadReaderSystem qr s,
ArrowQuery m q,
MonadSystem q s,
MonadIO s
) =>
s ()
handleInput = liftIO pollEvents >>= handleInput'
-- | Keyboard input system.
--
-- @since 0.5
handleInput' ::
(ArrowQueryReader qr, MonadReaderSystem qr s, ArrowQuery m q, MonadSystem q s) =>
[Event] ->
s ()
handleInput' events = do
let go (kbAcc, mouseAcc) event = case eventPayload event of
KeyboardEvent keyboardEvent ->
( case fromSDLKeycode $ keysymKeycode $ keyboardEventKeysym keyboardEvent of
Just key -> handleKeyboardEvent key (motionFromSDL $ keyboardEventKeyMotion keyboardEvent) . kbAcc
Nothing -> kbAcc,
mouseAcc
)
MouseMotionEvent mouseMotionEvent ->
( kbAcc,
\m ->
(mouseAcc m)
{ mousePosition = fromIntegral <$> mouseMotionEventPos mouseMotionEvent,
mouseOffset = fromIntegral <$> mouseMotionEventRelMotion mouseMotionEvent,
mouseButtons = mouseButtons m
}
)
MouseButtonEvent mouseButtonEvent ->
( kbAcc,
\m ->
(mouseAcc m)
{ mousePosition = fromIntegral <$> mouseButtonEventPos mouseButtonEvent,
mouseButtons =
Map.insert
(mouseButtonFromSDL $ mouseButtonEventButton mouseButtonEvent)
(motionFromSDL $ mouseButtonEventMotion mouseButtonEvent)
(mouseButtons m)
}
)
_ -> (kbAcc, mouseAcc)
(updateKb, updateMouse) = foldl' go (id, id) events
_ <- S.map () . Q.adjust $ const updateKb
_ <- S.map () . Q.adjust $ const updateMouse
return ()
-- | Convert an SDL mouse button to `MouseButton`.
--
-- @since 0.5
mouseButtonFromSDL :: SDL.MouseButton -> MouseButton
mouseButtonFromSDL b = case b of
SDL.ButtonLeft -> ButtonLeft
SDL.ButtonMiddle -> ButtonMiddle
SDL.ButtonRight -> ButtonRight
SDL.ButtonX1 -> ButtonX1
SDL.ButtonX2 -> ButtonX2
SDL.ButtonExtra i -> ButtonExtra i
-- | Convert a `MouseButton` to an SDL mouse button.
--
-- @since 0.5
mouseButtonToSDL :: MouseButton -> SDL.MouseButton
mouseButtonToSDL b = case b of
ButtonLeft -> SDL.ButtonLeft
ButtonMiddle -> SDL.ButtonMiddle
ButtonRight -> SDL.ButtonRight
ButtonX1 -> SDL.ButtonX1
ButtonX2 -> SDL.ButtonX2
ButtonExtra i -> SDL.ButtonExtra i
-- | Convert an SDL input motion to `InputMotion`.
--
-- @since 0.5
motionFromSDL :: SDL.InputMotion -> InputMotion
motionFromSDL m = case m of
SDL.Pressed -> Pressed
SDL.Released -> Released
-- | Convert an `InputMotion` to an SDL input motion.
--
-- @since 0.5
motionToSDL :: InputMotion -> SDL.InputMotion
motionToSDL m = case m of
Pressed -> SDL.Pressed
Released -> SDL.Released
-- | Convert an SDL key code to `Key`.
--
-- @since 0.5
toSDLKeycode :: Key -> SDL.Keycode
toSDLKeycode key = case key of
KeyA -> SDL.KeycodeA
KeyB -> SDL.KeycodeB
KeyC -> SDL.KeycodeC
KeyD -> SDL.KeycodeD
KeyE -> SDL.KeycodeE
KeyF -> SDL.KeycodeF
KeyG -> SDL.KeycodeG
KeyH -> SDL.KeycodeH
KeyI -> SDL.KeycodeI
KeyJ -> SDL.KeycodeJ
KeyK -> SDL.KeycodeK
KeyL -> SDL.KeycodeL
KeyM -> SDL.KeycodeM
KeyN -> SDL.KeycodeN
KeyO -> SDL.KeycodeO
KeyP -> SDL.KeycodeP
KeyQ -> SDL.KeycodeQ
KeyR -> SDL.KeycodeR
KeyS -> SDL.KeycodeS
KeyT -> SDL.KeycodeT
KeyU -> SDL.KeycodeU
KeyV -> SDL.KeycodeV
KeyW -> SDL.KeycodeW
KeyX -> SDL.KeycodeX
KeyY -> SDL.KeycodeY
KeyZ -> SDL.KeycodeZ
Key0 -> SDL.Keycode0
Key1 -> SDL.Keycode1
Key2 -> SDL.Keycode2
Key3 -> SDL.Keycode3
Key4 -> SDL.Keycode4
Key5 -> SDL.Keycode5
Key6 -> SDL.Keycode6
Key7 -> SDL.Keycode7
Key8 -> SDL.Keycode8
Key9 -> SDL.Keycode9
KeyF1 -> SDL.KeycodeF1
KeyF2 -> SDL.KeycodeF2
KeyF3 -> SDL.KeycodeF3
KeyF4 -> SDL.KeycodeF4
KeyF5 -> SDL.KeycodeF5
KeyF6 -> SDL.KeycodeF6
KeyF7 -> SDL.KeycodeF7
KeyF8 -> SDL.KeycodeF8
KeyF9 -> SDL.KeycodeF9
KeyF10 -> SDL.KeycodeF10
KeyF11 -> SDL.KeycodeF11
KeyF12 -> SDL.KeycodeF12
KeyEscape -> SDL.KeycodeEscape
KeyEnter -> SDL.KeycodeReturn
KeySpace -> SDL.KeycodeSpace
KeyBackspace -> SDL.KeycodeBackspace
KeyTab -> SDL.KeycodeTab
KeyCapsLock -> SDL.KeycodeCapsLock
KeyShift -> SDL.KeycodeLShift
KeyCtrl -> SDL.KeycodeLCtrl
KeyAlt -> SDL.KeycodeLAlt
KeyLeft -> SDL.KeycodeLeft
KeyRight -> SDL.KeycodeRight
KeyUp -> SDL.KeycodeUp
KeyDown -> SDL.KeycodeDown
KeyHome -> SDL.KeycodeHome
KeyEnd -> SDL.KeycodeEnd
KeyPageUp -> SDL.KeycodePageUp
KeyPageDown -> SDL.KeycodePageDown
KeyInsert -> SDL.KeycodeInsert
KeyDelete -> SDL.KeycodeDelete
KeyMinus -> SDL.KeycodeMinus
KeyEquals -> SDL.KeycodeEquals
KeyBracketLeft -> SDL.KeycodeLeftBracket
KeyBracketRight -> SDL.KeycodeRightBracket
KeyBackslash -> SDL.KeycodeBackslash
KeySemicolon -> SDL.KeycodeSemicolon
KeyComma -> SDL.KeycodeComma
KeyPeriod -> SDL.KeycodePeriod
KeySlash -> SDL.KeycodeSlash
KeyNumLock -> SDL.KeycodeNumLockClear
KeyNumpad0 -> SDL.KeycodeKP0
KeyNumpad1 -> SDL.KeycodeKP1
KeyNumpad2 -> SDL.KeycodeKP2
KeyNumpad3 -> SDL.KeycodeKP3
KeyNumpad4 -> SDL.KeycodeKP4
KeyNumpad5 -> SDL.KeycodeKP5
KeyNumpad6 -> SDL.KeycodeKP6
KeyNumpad7 -> SDL.KeycodeKP7
KeyNumpad8 -> SDL.KeycodeKP8
KeyNumpad9 -> SDL.KeycodeKP9
KeyNumpadDivide -> SDL.KeycodeKPDivide
KeyNumpadMultiply -> SDL.KeycodeKPMultiply
KeyNumpadMinus -> SDL.KeycodeKPMinus
KeyNumpadPlus -> SDL.KeycodeKPPlus
KeyNumpadEnter -> SDL.KeycodeKPEnter
KeyNumpadPeriod -> SDL.KeycodeKPPeriod
KeySuper -> SDL.KeycodeLGUI
KeyMenu -> SDL.KeycodeMenu
-- | Convert a `Key` to an SDL key code.
--
-- @since 0.5
fromSDLKeycode :: SDL.Keycode -> Maybe Key
fromSDLKeycode keycode = case keycode of
SDL.KeycodeA -> Just KeyA
SDL.KeycodeB -> Just KeyB
SDL.KeycodeC -> Just KeyC
SDL.KeycodeD -> Just KeyD
SDL.KeycodeE -> Just KeyE
SDL.KeycodeF -> Just KeyF
SDL.KeycodeG -> Just KeyG
SDL.KeycodeH -> Just KeyH
SDL.KeycodeI -> Just KeyI
SDL.KeycodeJ -> Just KeyJ
SDL.KeycodeK -> Just KeyK
SDL.KeycodeL -> Just KeyL
SDL.KeycodeM -> Just KeyM
SDL.KeycodeN -> Just KeyN
SDL.KeycodeO -> Just KeyO
SDL.KeycodeP -> Just KeyP
SDL.KeycodeQ -> Just KeyQ
SDL.KeycodeR -> Just KeyR
SDL.KeycodeS -> Just KeyS
SDL.KeycodeT -> Just KeyT
SDL.KeycodeU -> Just KeyU
SDL.KeycodeV -> Just KeyV
SDL.KeycodeW -> Just KeyW
SDL.KeycodeX -> Just KeyX
SDL.KeycodeY -> Just KeyY
SDL.KeycodeZ -> Just KeyZ
SDL.Keycode0 -> Just Key0
SDL.Keycode1 -> Just Key1
SDL.Keycode2 -> Just Key2
SDL.Keycode3 -> Just Key3
SDL.Keycode4 -> Just Key4
SDL.Keycode5 -> Just Key5
SDL.Keycode6 -> Just Key6
SDL.Keycode7 -> Just Key7
SDL.Keycode8 -> Just Key8
SDL.Keycode9 -> Just Key9
SDL.KeycodeF1 -> Just KeyF1
SDL.KeycodeF2 -> Just KeyF2
SDL.KeycodeF3 -> Just KeyF3
SDL.KeycodeF4 -> Just KeyF4
SDL.KeycodeF5 -> Just KeyF5
SDL.KeycodeF6 -> Just KeyF6
SDL.KeycodeF7 -> Just KeyF7
SDL.KeycodeF8 -> Just KeyF8
SDL.KeycodeF9 -> Just KeyF9
SDL.KeycodeF10 -> Just KeyF10
SDL.KeycodeF11 -> Just KeyF11
SDL.KeycodeF12 -> Just KeyF12
SDL.KeycodeEscape -> Just KeyEscape
SDL.KeycodeReturn -> Just KeyEnter
SDL.KeycodeSpace -> Just KeySpace
SDL.KeycodeBackspace -> Just KeyBackspace
SDL.KeycodeTab -> Just KeyTab
SDL.KeycodeCapsLock -> Just KeyCapsLock
SDL.KeycodeLShift -> Just KeyShift
SDL.KeycodeRShift -> Just KeyShift
SDL.KeycodeLCtrl -> Just KeyCtrl
SDL.KeycodeRCtrl -> Just KeyCtrl
SDL.KeycodeLAlt -> Just KeyAlt
SDL.KeycodeRAlt -> Just KeyAlt
SDL.KeycodeLeft -> Just KeyLeft
SDL.KeycodeRight -> Just KeyRight
SDL.KeycodeUp -> Just KeyUp
SDL.KeycodeDown -> Just KeyDown
SDL.KeycodeHome -> Just KeyHome
SDL.KeycodeEnd -> Just KeyEnd
SDL.KeycodePageUp -> Just KeyPageUp
SDL.KeycodePageDown -> Just KeyPageDown
SDL.KeycodeInsert -> Just KeyInsert
SDL.KeycodeDelete -> Just KeyDelete
SDL.KeycodeMinus -> Just KeyMinus
SDL.KeycodeEquals -> Just KeyEquals
SDL.KeycodeLeftBracket -> Just KeyBracketLeft
SDL.KeycodeRightBracket -> Just KeyBracketRight
SDL.KeycodeBackslash -> Just KeyBackslash
SDL.KeycodeSemicolon -> Just KeySemicolon
SDL.KeycodeComma -> Just KeyComma
SDL.KeycodePeriod -> Just KeyPeriod
SDL.KeycodeSlash -> Just KeySlash
SDL.KeycodeNumLockClear -> Just KeyNumLock
SDL.KeycodeKP0 -> Just KeyNumpad0
SDL.KeycodeKP1 -> Just KeyNumpad1
SDL.KeycodeKP2 -> Just KeyNumpad2
SDL.KeycodeKP3 -> Just KeyNumpad3
SDL.KeycodeKP4 -> Just KeyNumpad4
SDL.KeycodeKP5 -> Just KeyNumpad5
SDL.KeycodeKP6 -> Just KeyNumpad6
SDL.KeycodeKP7 -> Just KeyNumpad7
SDL.KeycodeKP8 -> Just KeyNumpad8
SDL.KeycodeKP9 -> Just KeyNumpad9
SDL.KeycodeKPDivide -> Just KeyNumpadDivide
SDL.KeycodeKPMultiply -> Just KeyNumpadMultiply
SDL.KeycodeKPMinus -> Just KeyNumpadMinus
SDL.KeycodeKPPlus -> Just KeyNumpadPlus
SDL.KeycodeKPEnter -> Just KeyNumpadEnter
SDL.KeycodeKPPeriod -> Just KeyNumpadPeriod
SDL.KeycodeLGUI -> Just KeySuper
SDL.KeycodeRGUI -> Just KeySuper
SDL.KeycodeMenu -> Just KeyMenu
_ -> Nothing