packages feed

aztecs-sdl-0.1.0.0: src/Data/Aztecs/SDL.hs

{-# LANGUAGE Arrows #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}

module Data.Aztecs.SDL where

import Control.Arrow (Arrow (..), returnA, (>>>))
import Data.Aztecs
import qualified Data.Aztecs.Access as A
import Data.Aztecs.Asset (Asset (..), AssetServer, Handle, lookupAsset)
import qualified Data.Aztecs.Asset as Asset
import qualified Data.Aztecs.Query as Q
import qualified Data.Aztecs.System as S
import Data.Aztecs.Transform (Transform (..))
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe (mapMaybe)
import qualified Data.Text as T
import Foreign.C (CInt)
import SDL hiding (Texture, Window, windowTitle)
import qualified SDL hiding (Texture)
import qualified SDL.Image as IMG

-- | Window component.
data Window = Window
  { windowTitle :: String
  }
  deriving (Show)

instance Component Window

-- | Window renderer component.
data WindowRenderer = WindowRenderer
  { windowRendererRaw :: SDL.Window,
    windowRenderer :: Renderer
  }
  deriving (Show)

instance Component WindowRenderer

-- | Setup SDL
setup :: System () ()
setup =
  fmap (const ()) $
    Asset.setup @Texture
      &&& S.run (const initializeAll)
      &&& S.queue (const . A.spawn_ . bundle $ Keyboard mempty)

-- | Update SDL windows
update :: System () ()
update =
  addWindows
    >>> addWindowTargets
    >>> Asset.loadAssets @Texture
    >>> keyboardInput

draw :: System () ()
draw = drawImages >>> renderWindows

-- | Setup new windows.
addWindows :: System () ()
addWindows = proc () -> do
  newWindows <- S.filter (Q.entity &&& Q.fetch @_ @Window) (without @WindowRenderer) -< ()
  newWindows' <- S.run createNewWindows -< newWindows
  S.queue insertNewWindows -< newWindows'
  where
    createNewWindows newWindows = mapM createWindowRenderer newWindows
    createWindowRenderer (eId, window) = do
      sdlWindow <- createWindow (T.pack $ windowTitle window) defaultWindow
      renderer <- createRenderer sdlWindow (-1) defaultRenderer
      return (eId, sdlWindow, renderer)
    insertNewWindows newWindows' = mapM_ insertWindowRenderer newWindows'
    insertWindowRenderer (eId, window, renderer) = A.insert eId (WindowRenderer window renderer)

-- | Render windows.
renderWindows :: System () ()
renderWindows =
  let go windowDraws =
        mapM_
          ( \(window, draws) -> do
              let renderer = windowRenderer window
              rendererDrawColor renderer $= V4 0 0 0 255
              clear renderer
              mapM_ (\(d, transform) -> runDraw d transform renderer) draws
              present renderer
          )
          windowDraws
   in proc () -> do
        windows <- S.all (Q.entity &&& Q.fetch @_ @WindowRenderer) -< ()
        draws <-
          S.all
            ( proc () -> do
                d <- Q.fetch @_ @Draw -< ()
                transform <- Q.fetch @_ @Transform -< ()
                target <- Q.fetch @_ @WindowTarget -< ()
                returnA -< (d, transform, target)
            )
            -<
              ()
        let windowDraws =
              foldr
                ( \(eId, window) acc ->
                    let draws' =
                          foldr
                            ( \(d, transform, target) acc' ->
                                if unWindowTarget target == eId
                                  then (d, transform) : acc'
                                  else acc'
                            )
                            []
                            draws
                     in (window, draws') : acc
                )
                []
                windows
        S.run go -< windowDraws

-- | Window target component.
-- This component can be used to specify which `Window` to draw an entity to.
newtype WindowTarget = WindowTarget {unWindowTarget :: EntityID}
  deriving (Eq, Show)

instance Component WindowTarget

-- | Draw component.
-- This component can be used to draw to a window.
newtype Draw = Draw {runDraw :: Transform -> Renderer -> IO ()}

instance Component Draw

-- | Add `WindowTarget` components to entities with a new `Draw` component.
addWindowTargets :: System () ()
addWindowTargets = proc () -> do
  windows <- S.all (Q.entity &&& Q.fetch @_ @WindowRenderer) -< ()
  newDraws <- S.filter (Q.entity &&& Q.fetch @_ @Draw) (without @WindowTarget) -< ()
  S.queue
    ( \(newDraws, windows) -> case windows of
        (windowEId, _) : _ -> mapM_ (\(eId, _) -> A.insert eId $ WindowTarget windowEId) newDraws
        _ -> return ()
    )
    -<
      (newDraws, windows)

-- | Draw a rectangle.
rect :: V2 Int -> Draw
rect size = Draw $
  \transform renderer -> do
    surface <- createRGBSurface (fmap fromIntegral size) RGBA8888
    surfaceRenderer <- createSoftwareRenderer surface
    rendererDrawColor surfaceRenderer $= V4 255 0 0 255
    fillRect surfaceRenderer Nothing
    texture <- SDL.createTextureFromSurface renderer surface
    copyEx
      renderer
      texture
      Nothing
      (Just (Rectangle (fmap (fromIntegral @CInt . round) . P $ transformPosition transform) (fmap fromIntegral size)))
      (realToFrac $ transformRotation transform)
      Nothing
      (V2 False False)
    freeSurface surface
    destroyTexture texture

-- | Texture asset.
newtype Texture = Texture {textureSurface :: Surface}

instance Asset Texture where
  loadAsset path = Texture <$> IMG.load path

-- | Image component.
data Image = Image
  { imageTexture :: Handle Texture,
    imageSize :: V2 Int
  }
  deriving (Show)

instance Component Image

-- | Draw images to their target windows.
drawImages :: System () ()
drawImages = proc () -> do
  imgs <- S.filter (Q.entity &&& Q.fetch @_ @Image) (without @Draw) -< ()
  assets <- S.single (Q.fetch @_ @(AssetServer Texture)) -< ()
  let newAssets =
        mapMaybe (\(eId, img) -> (,img,eId) <$> lookupAsset (imageTexture img) assets) imgs
  S.queue (mapM_ go) -< newAssets
  where
    go (texture, img, eId) = do
      A.insert
        eId
        ( Draw $
            \transform renderer -> do
              sdlTexture <- SDL.createTextureFromSurface renderer (textureSurface texture)
              copyEx
                renderer
                sdlTexture
                Nothing
                ( Just
                    ( Rectangle
                        (fmap (fromIntegral @CInt . round) . P $ transformPosition transform)
                        (fmap fromIntegral $ imageSize img)
                    )
                )
                (realToFrac $ transformRotation transform)
                Nothing
                (V2 False False)
              destroyTexture sdlTexture
        )

-- | Keyboard state.
newtype Keyboard = Keyboard {unKeyboard :: Map Keycode InputMotion}
  deriving (Show, Semigroup, Monoid)

instance Component Keyboard

-- | Keyboard input system.
keyboardInput :: System () ()
keyboardInput = proc () -> do
  events <- S.run . const $ SDL.pollEvents -< ()
  S.mapSingle
    ( proc events -> do
        let go event keyAcc = case eventPayload event of
              KeyboardEvent keyboardEvent ->
                Keyboard $
                  Map.insert
                    (keysymKeycode $ keyboardEventKeysym keyboardEvent)
                    (keyboardEventKeyMotion keyboardEvent)
                    (unKeyboard keyAcc)
              _ -> keyAcc
        keyboard <- Q.fetch -< ()
        Q.set -< foldr go keyboard events
    )
    -<
      events
  returnA -< ()