packages feed

pine-0.1.0.3: src/Pine/Internal/Pine.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE LambdaCase #-}

module Pine.Internal.Pine
  ( pine
  , withDefaultConfig
  , with
  ) where

import Pine.Internal.Types

import qualified SDL
import qualified SDL.Image as SDLI
import Control.Concurrent.STM

import Data.Text (Text)
import Data.Map (Map)
import qualified Data.Map as M
import Control.Monad
import Control.Monad.State
import Data.Word (Word32)

type TextureCache = Map FilePath SDL.Texture

-- | This function initializes the window and takes an initial `Stateful` object that will be updated.
pine :: (Stateful s, Drawable s)
     => Text -- ^ Title
     -> SDL.WindowConfig -- ^ Window Configuration
     -> s -- ^ Initial state
     -> IO ()
pine title windowConfig state_ = do
  SDL.initializeAll
  window <- SDL.createWindow title windowConfig
  renderer <- SDL.createRenderer window (-1) $ SDL.RendererConfig
    { SDL.rendererType = SDL.AcceleratedRenderer
    , SDL.rendererTargetTexture = False
    }
  let
    appLoop :: TextureCache -> IO ()
    appLoop cache = do
      updateQueue <- newTChanIO
      timer <- SDL.addTimer 16 (fpsTimer updateQueue)
      time <- SDL.ticks
      dcache <- updateEvents (fromIntegral time / 1000) [Load] state_ >>= \case
        Just loadState -> SDL.pollEvents >>= appStep time updateQueue cache loadState
        Nothing        -> pure cache
      _ <- SDL.removeTimer timer
      foldMap SDL.destroyTexture dcache
      SDL.destroyRenderer renderer
      SDL.destroyWindow window

    appStep :: (Stateful s, Drawable s) => Word32 -> TChan () -> TextureCache -> s -> [SDL.Event] -> IO TextureCache
    appStep time updateQueue cache state sdlEvents = do
      atomically $ readTChan updateQueue -- not ideal, events could get backed up
      time' <- SDL.ticks
      let dt = (fromIntegral (time' - time) :: Double) / 1000
      --print $ 1/dt
      cache' <- drawScene cache $ draw state
--      mousePos <- SDL.getModalMouseLocation >>= \case  --waiting for PR to be merged on sdl2
--        SDL.AbsoluteModalLocation (SDL.P pos) -> pos
--        SDL.RelativeModalLocation pos -> pos
      let pineEvents = eventState sdlEvents
      updateEvents dt (Step:pineEvents) state >>= \case
        Just newState -> SDL.pollEvents >>= appStep time' updateQueue cache' newState
        Nothing       -> pure cache'
      where
        eventState events =
          case events of
            [] -> []
            (ev:evs) ->
              [SDLEvent ev] <> case SDL.eventPayload ev of
                SDL.WindowClosedEvent _ -> [WindowClose] <> (eventState evs)
                SDL.KeyboardEvent keyboardEvent ->
                  case SDL.keyboardEventKeyMotion keyboardEvent of
                    SDL.Pressed  -> [KeyPressed  (SDL.keysymKeycode (SDL.keyboardEventKeysym keyboardEvent))] <> (eventState evs)
                    SDL.Released -> [KeyReleased (SDL.keysymKeycode (SDL.keyboardEventKeysym keyboardEvent))] <> (eventState evs)
                SDL.MouseMotionEvent mmeData -> [MouseMoved (SDL.mouseMotionEventRelMotion mmeData)] <> (eventState evs) -- maybe include new position as well
                _ -> (eventState evs)

    updateEvents :: (Stateful s, Drawable s) => DeltaTime -> [Event] -> s -> IO (Maybe s)
    updateEvents _  []     state = pure $ Just state
    updateEvents dt (e:es) state = do
      let (r, nState) = runState (update dt e) state
      case r of
        Cont          -> updateEvents dt es nState
        Log s         -> putStrLn s *> (updateEvents dt es nState)
        QuitWithLog s -> putStrLn s *> (pure $ Nothing)
        Quit          -> pure $ Nothing

    fpsTimer :: TChan () -> Word32 -> IO SDL.RetriggerTimer
    fpsTimer updateQueue _ = do
      atomically $ writeTChan updateQueue ()
      pure $ SDL.Reschedule 16

    drawScene :: TextureCache -> Scene -> IO TextureCache
    drawScene cache canvas = do
      SDL.clear renderer
      cache' <- drawScene' cache canvas
      SDL.present renderer
      pure cache'

    drawScene' :: TextureCache -> Scene -> IO TextureCache
    drawScene' cache canvas = do
      case canvas of
        SingleScene m -> loadMedia cache [m]
        MultiScene ms -> loadMedia cache ms
        EmptyScene    -> pure cache

    loadMedia :: TextureCache -> [Media] -> IO TextureCache
    loadMedia cache [] = pure cache
    loadMedia cache (m:imgs) =
      case m of
        MImage img ->
          case cache M.!? (imageSrc img) of
            Nothing -> do
              tex <- SDLI.loadTexture renderer (imageSrc img)
              SDL.copy renderer tex Nothing Nothing
              loadMedia (M.insert (imageSrc img) tex cache) imgs
            Just tex -> do
              SDL.copy renderer tex (imageQuad img) (imageRect img)
              loadMedia cache imgs
        _ -> loadMedia cache imgs

   in appLoop mempty

withDefaultConfig = SDL.WindowConfig
  { SDL.windowBorder        = True
  , SDL.windowHighDPI       = False
  , SDL.windowInputGrabbed  = False
  , SDL.windowMode          = SDL.Windowed
  , SDL.windowGraphicsContext = SDL.NoGraphicsContext
  , SDL.windowPosition      = SDL.Wherever
  , SDL.windowResizable     = True
  , SDL.windowInitialSize   = SDL.V2 800 800
  , SDL.windowVisible       = True
  }

with :: (MonadState s m) => (s -> (a,s)) -> m a
with = state