rhine-gloss-1.4: src/FRP/Rhine/Gloss/IO.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}
-- | Wrapper to write @gloss@ applications in Rhine, using concurrency.
module FRP.Rhine.Gloss.IO (
GlossEnv (..),
GlossConcT (..),
GlossConc,
runGlossConcT,
paintIO,
clearIO,
paintAllIO,
GlossEventClockIO (..),
GlossSimClockIO (..),
makeGlossEnv,
launchInGlossThread,
launchGlossThread,
flowGlossIO,
runGlossEnvClock,
RunGlossEnvClock,
GlossClockUTC,
glossClockUTC,
GlossConcTClock,
glossConcTClock,
GlossConcClock,
glossConcClock,
)
where
-- base
import Control.Concurrent
import Data.Functor (void)
import Data.IORef
import System.Timeout (timeout)
-- transformers
import Control.Monad.Trans.Class
import Control.Monad.Trans.Reader
-- gloss
import Graphics.Gloss.Interface.IO.Game
-- monad-schedule
import Control.Monad.Schedule.Class
import Control.Monad.Schedule.FreeAsync
-- rhine
import FRP.Rhine
import FRP.Rhine.Clock.Realtime (UTCClock, addUTC)
-- rhine-gloss
import FRP.Rhine.Gloss.Common
-- * Gloss effects
-- | Concurrent variables needed to communicate with the gloss backend.
data GlossEnv = GlossEnv
{ timeVar :: MVar Float
, eventVar :: MVar Event
, picRef :: IORef Picture
, timeRef :: IORef Float
}
{- | Effects in the gloss backend
* Wraps the concurrent variables needed for communication with the @gloss@ backend.
* Adds the 'FreeAsyncT' concurrency layer for fairer scheduling
-}
newtype GlossConcT m a = GlossConcT
{unGlossConcT :: ReaderT GlossEnv (FreeAsyncT m) a}
deriving (Functor, Applicative, Monad, MonadIO)
-- | When @gloss@ is the only effect you are using, use this monad to simplify your type signatures.
type GlossConc = GlossConcT IO
instance MonadTrans GlossConcT where
lift = GlossConcT . lift . lift
-- FIXME MFunctor & MMonad instances pending https://github.com/HeinrichApfelmus/operational/pull/28/
-- | Remove the 'GlossConcT' transformer by explicitly providing an environment.
runGlossConcT :: (MonadIO m) => GlossConcT m a -> GlossEnv -> m a
runGlossConcT ma env = runFreeAsyncT $ runReaderT (unGlossConcT ma) env
-- | Disregards scheduling capabilities of @m@, as it uses 'FreeAsync'.
instance (MonadIO m) => MonadSchedule (GlossConcT m) where
schedule actions = GlossConcT $ fmap (second $ map GlossConcT) $ schedule $ unGlossConcT <$> actions
withPicRef ::
(MonadIO m) =>
(IORef Picture -> IO a) ->
GlossConcT m a
withPicRef action = GlossConcT $ do
GlossEnv {picRef} <- ask
liftIO $ action picRef
-- | Add a picture to the canvas.
paintIO :: (MonadIO m) => Picture -> GlossConcT m ()
paintIO pic = withPicRef $ \ref -> modifyIORef' ref (<> pic)
-- | Clear the canvas.
clearIO :: (MonadIO m) => GlossConcT m ()
clearIO = withPicRef $ \ref -> writeIORef ref Blank
-- | Clear the canvas and then paint.
paintAllIO :: (MonadIO m) => Picture -> GlossConcT m ()
paintAllIO pic = withPicRef $ \ref -> writeIORef ref pic
-- * Gloss clocks in 'IO'
{- | Concurrently block on @gloss@ events.
Caution: Currently, you should only add one such clock in a 'Rhine'.
If you add several 'GlossEventClockIO', only one will be chosen at random and receive the event.
See https://github.com/turion/rhine/issues/330.
-}
data GlossEventClockIO = GlossEventClockIO
instance (MonadIO m) => Clock (GlossConcT m) GlossEventClockIO where
type Time GlossEventClockIO = Float
type Tag GlossEventClockIO = Event
initClock _ = return (constM getEvent, 0)
where
getEvent = do
GlossEnv {eventVar, timeRef} <- GlossConcT ask
event <- GlossConcT $ lift $ asyncMVar eventVar
liftIO $ do
time <- readIORef timeRef
return (time, event)
instance GetClockProxy GlossEventClockIO
{- | Concurrently block on @gloss@ simulation ticks.
Caution: Currently, you should only add one such clock in a 'Rhine'.
If you add several 'GlossSimClockIO', only one will be chosen at random and receive the event.
See https://github.com/turion/rhine/issues/330.
-}
data GlossSimClockIO = GlossSimClockIO
instance (MonadIO m) => Clock (GlossConcT m) GlossSimClockIO where
type Time GlossSimClockIO = Float
type Tag GlossSimClockIO = ()
initClock _ = return (constM getTime &&& arr (const ()), 0)
where
getTime = GlossConcT $ do
GlossEnv {timeVar} <- ask
lift $ asyncMVar timeVar
instance GetClockProxy GlossSimClockIO
-- * Reactimation
{- | Create the concurrent variables to communicate with the @gloss@ backend.
You will usually not need this function, have a look at 'launchInGlossThread' and 'flowGlossIO' instead.
-}
makeGlossEnv ::
(MonadIO m) =>
m GlossEnv
makeGlossEnv = liftIO $ GlossEnv <$> newEmptyMVar <*> newEmptyMVar <*> newIORef Blank <*> newIORef 0
{- | Helper function for 'launchInGlossThread'.
Creates concurrent variables and launches the @gloss@ backend in a separate thread.
-}
launchGlossThread ::
(MonadIO m) =>
GlossSettings ->
m GlossEnv
launchGlossThread GlossSettings {..} = do
vars <- makeGlossEnv
let
getPic GlossEnv {picRef} = readIORef picRef
handleEvent event vars@GlossEnv {eventVar} = do
void $
forkIO $ -- Perform non-blocking so other actions are not delayed
void $
timeout 100000 $ -- timeout in case noone is listening for events
putMVar eventVar event
return vars
simStep diffTime vars@GlossEnv {timeVar, timeRef} = do
time <- readIORef timeRef
let !time' = time + diffTime
-- We don't do this in a separate thread, because forkIO putMVar would create a race condition on putting the MVar,
-- which can lead to non-monotonous time updates.
tryPutMVar timeVar time'
writeIORef timeRef time'
return vars
void $ liftIO $ forkIO $ playIO display backgroundColor stepsPerSecond vars getPic handleEvent simStep
return vars
{- | Apply this to supply the 'GlossConcT' effect.
Creates a new thread in which @gloss@ is run,
and feeds the clocks 'GlossEventClockIO' and 'GlossSimClockIO'.
Usually, this function is applied to the result of 'flow',
so you can handle all occurring effects as needed.
If you only use @gloss@ in your whole signal network,
you can use 'flowGlossIO' instead.
-}
launchInGlossThread ::
(MonadIO m) =>
GlossSettings ->
GlossConcT m a ->
m a
launchInGlossThread settings glossLoop = do
vars <- launchGlossThread settings
runGlossConcT glossLoop vars
{- | Run a 'Rhine' in the 'GlossConcT' monad by launching a separate thread for the @gloss@ backend,
and reactimate in the foreground.
-}
flowGlossIO ::
( MonadIO m
, Clock (GlossConcT m) cl
, GetClockProxy cl
, Time cl ~ Time (In cl)
, Time cl ~ Time (Out cl)
) =>
GlossSettings ->
Rhine (GlossConcT m) cl () () ->
m ()
flowGlossIO settings = launchInGlossThread settings . flow
{- | Apply this wrapper to your clock type @cl@ in order to escape the 'GlossConcT' transformer.
The resulting clock will be in @m@, not 'GlossConcT m' anymore.
Typically, @m@ will have the 'MonadIO' constraint.
-}
type RunGlossEnvClock m cl = HoistClock (GlossConcT m) m cl
{- | Apply to a gloss clock to remove a 'GlossConcT' layer.
You will have to have initialized a 'GlossEnv', for example by calling 'launchGlossThread'.
-}
runGlossEnvClock ::
(MonadIO m) =>
GlossEnv ->
cl ->
RunGlossEnvClock m cl
runGlossEnvClock env unhoistedClock =
HoistClock
{ monadMorphism = flip runGlossConcT env
, ..
}
-- * Lifting clocks to 'GlossConcT'
{- | Lift a 'MonadIO' clock to 'GlossConcT'.
You should use this instead of 'IOClock', otherwise scheduling will probably not work.
(This is because 'GlossConcT' uses 'FreeAsyncT', but 'liftIO' is not asynchronous.)
-}
type GlossConcTClock m = HoistClock IO (GlossConcT m)
-- | A 'MonadIO' clock lifted to 'GlossConcT'.
glossConcTClock :: (MonadIO m) => cl -> GlossConcTClock m cl
glossConcTClock unhoistedClock =
HoistClock
{ unhoistedClock
, monadMorphism = GlossConcT . lift . freeAsync
}
{- | Lift an 'IO' clock to 'GlossConc'.
See 'GlossConcTClock'.
-}
type GlossConcClock = GlossConcTClock IO
-- | An 'IO' clock lifted to 'GlossConc'.
glossConcClock :: cl -> GlossConcClock cl
glossConcClock = glossConcTClock
-- * Rescaled clocks in other time domains
{- | Rescale a gloss clock like 'GlossSimClockIO' or 'GlossEventClockIO' to 'UTCTime'.
This is needed for compatibility with other realtime clocks like 'Millisecond'.
-}
type GlossClockUTC m cl = UTCClock (GlossConcT m) cl
{- | Rescale a gloss clock like 'GlossSimClockIO' or 'GlossEventClockIO' to 'UTCTime'.
Uses 'addUTC'. For other strategies to rescale a gloss clock to 'UTCTime',
see "FRP.Rhine.Clock.Realtime".
-}
glossClockUTC :: (MonadIO m, Real (Time cl)) => cl -> GlossClockUTC m cl
glossClockUTC = addUTC