packages feed

ghcjs-vdom-0.2.0.0: src/GHCJS/VDOM/Render.hs

{-# LANGUAGE TypeFamilies, QuasiQuotes, ScopedTypeVariables #-}
{-|
   Some utilities to manage a render queue for ghcjs-vdom components.

   Diffing is performed in an asynchronous background thread. When a
   patch is ready, an animationframe is requested to apply the update.

   Using the render queue is optional, directly calling diff and patch
   for the components is a lower level way to update the document.
 -}

module GHCJS.VDOM.Render ( Renderer
                         , render
                         , mkRenderer
                         ) where

import           GHCJS.Foreign.Callback
import           GHCJS.Foreign.QQ
import           GHCJS.Types

import           GHCJS.VDOM.Internal.Types
import qualified GHCJS.VDOM.Component as C
import qualified GHCJS.VDOM as V

import           JavaScript.Web.AnimationFrame

import           Control.Concurrent
import qualified Control.Exception as E
import           Control.Monad

import           Data.IORef
import           Data.Map (Map)
import qualified Data.Map as M
import           Data.Typeable

class Renderable a where
  type Render a
  doRender :: Renderer -> a -> Render a

instance Renderable VComp where
  type Render VComp = IO ()
  doRender r c@(VComp vcj) =
    enqueueRender r vcj (C.render c >>= C.diff c >>= addPatch vcj) (flushPatch vcj)

instance Renderable VMount where
  type Render VMount = (VNode -> IO ())
  doRender r vm@(VMount vmj) vn =
    enqueueRender r vmj (V.diff vm vn >>= addPatch vmj) (flushPatch vmj)

render :: Renderable a => Renderer -> a -> Render a
render r x = doRender r x

mkRenderer :: IO Renderer
mkRenderer = do
  r <- Renderer <$> newIORef M.empty
                <*> newIORef M.empty
                <*> newChan
  void (forkIO (renderThread r))
  return r

data Renderer = Renderer
  { renderPending      :: IORef (Map Double (IO (), IO ()))
  , renderFlushPending :: IORef (Map Double (IO ()))
  , renderQueue        :: Chan Double
  } deriving (Typeable)

renderThread :: Renderer -> IO ()
renderThread r = forever $ do
  k <- readChan (renderQueue r)
  Just (compute, flush) <-
    atomicModifyIORef (renderPending r) (\m -> (M.delete k m, M.lookup k m))
  let actions = do
        compute
        m <- atomicModifyIORef (renderFlushPending r)
                               (\m -> (M.insert k flush m, m))
        when (M.null m) (void $ inAnimationFrame ThrowWouldBlock
                                                 (\_ -> flushPatches r))
  actions`E.catch` \(_::E.SomeException) -> return ()

flushPatches :: Renderer -> IO ()
flushPatches r =
  mapM_ (\m -> m `E.catch` \(_::E.SomeException) -> return ()) =<<
    atomicModifyIORef (renderFlushPending r)
                      (\m -> (M.empty, M.elems m))

enqueueRender :: Renderer
              -> JSVal
              -> IO ()
              -> IO ()
              -> IO ()
enqueueRender r renderable compute flush = do
  k <- renderableKey renderable
  m <- atomicModifyIORef (renderPending r)
                         (\m -> (M.insert k (compute, flush) m, m))
  when (M.notMember k m) (writeChan (renderQueue r) k)

renderableKey :: JSVal -> IO Double
renderableKey r = [jsu| `r._key |]

addPatch :: JSVal -> Patch -> IO ()
addPatch r (Patch p) = [jsu_| `r.addPatch(`p); |]

flushPatch :: JSVal -> IO ()
flushPatch r = [jsu_| `r.patch(null); |]