glazier-pipes-0.1.5.1: src/Glazier/Pipes/Ui.hs
{-# LANGUAGE RankNTypes #-}
module Glazier.Pipes.Ui where
import Control.Concurrent.STM
import Control.Concurrent.STM.TMVar.Extras as STE
import Control.Concurrent
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Morph
import Control.Monad.Trans.Maybe
import qualified Pipes as P
import qualified Pipes.Prelude as PP
-- | This is similar to part of the Elm startApp.
-- This is responsible for running the Glazier Gadget update tick until it quits.
-- This is also responsible for rendering the frame.
-- This function is only required if you are not using another application framework which
-- already takes care of rendering and processing user input.
runUi :: (MonadIO io) =>
Int
-> (s -> IO ()) -- render
-> P.Producer s io s
-> io s
runUi refreshDelay render appSignal = do
-- framerate thread
-- TMVar to indicate that the render thread can render, start non empty so we can render straight away.
triggerRender <- liftIO $ newTMVarIO ()
frameRateThread <-
liftIO $
forkIO . void . forever $
-- The will ensure a refreshDelay in between times when value in canRender is taken.
-- wait until canRender is empty (ie taken by the render thread)
do
atomically $ STE.waitTillEmptyTMVar triggerRender ()
-- if empty, then wait delay before filling TMVar with next canRender value
threadDelay refreshDelay
atomically $ putTMVar triggerRender ()
-- render thread
enableRenderThread <- liftIO $ newTMVarIO ()
finishedRenderThread <- liftIO newEmptyTMVarIO
latestState <- liftIO newEmptyTMVarIO
void . liftIO $
forkFinally
(void . runMaybeT . forever $
-- check if we can start render
do
liftIO . atomically . void $ takeTMVar triggerRender
-- to allow rendering of last frame before quitting
-- if there is no state to render, check if rendering is disabled
s <-
MaybeT . liftIO . atomically $
(Just <$> takeTMVar latestState) `orElse` do
r <- tryReadTMVar enableRenderThread
case r of
Nothing -> pure Nothing -- breaks (runMaybeT . forever) loop
Just _ -> retry
lift $ render s)
(const . atomically $ putTMVar finishedRenderThread ())
-- application run
s' <- P.runEffect $
appSignal P.>-> PP.mapM
(liftIO . atomically . void . STE.forceSwapTMVar latestState) P.>-> PP.drain
-- cleanup
-- allow rendering of the frame one last time
liftIO . atomically $ takeTMVar enableRenderThread
-- wait for render thread to finish before exiting
liftIO . atomically $ takeTMVar finishedRenderThread
-- kill frameRateThread only after render thread has finished
-- since renderThread waits on triggers from frameRateThread
liftIO $ killThread frameRateThread
-- return final state
liftIO $ pure s'