packages feed

reflex-gi-gtk-0.1.0.0: src/Reflex/GI/Gtk/Run/Base.hs

-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.

{-# LANGUAGE GeneralizedNewtypeDeriving, RecordWildCards, UndecidableInstances, TypeFamilies #-}
{-# LANGUAGE StandaloneDeriving, FlexibleInstances, MultiParamTypeClasses, TypeApplications #-}
{-# LANGUAGE PatternSynonyms #-}

module Reflex.GI.Gtk.Run.Base
  ( RunGtkT
  , runGtkT
  ) where

import Control.Concurrent ( ThreadId
                          , myThreadId
                          , newEmptyMVar
                          , putMVar
                          , readMVar
                          )
import Control.Concurrent.STM.TChan ( TChan
                                    , newTChanIO
                                    , readTChan
                                    , tryReadTChan
                                    , writeTChan
                                    )
import Control.Concurrent.STM.TVar ( newTVarIO
                                   , readTVar
                                   , writeTVar
                                   )
import Control.Exception ( SomeException
                         , catch
                         , mask_
                         , throwIO
                         , try
                         )
import Control.Monad ( join
                     , void
                     )
import Control.Monad.Exception (MonadException)
import Control.Monad.Fix (MonadFix)
import Control.Monad.IO.Class ( MonadIO
                              , liftIO
                              )
import Control.Monad.Reader ( ReaderT
                            , asks
                            , runReaderT
                            )
import Control.Monad.Ref (MonadRef)
import Control.Monad.STM ( STM
                         , atomically
                         , orElse
                         , retry
                         , throwSTM
                         )
import Control.Monad.Trans (MonadTrans)
import Data.Function (fix)
import GI.GLib.Constants ( pattern PRIORITY_HIGH_IDLE
                         , pattern SOURCE_REMOVE
                         )
import GI.Gdk (threadsAddIdle)
import Reflex ( Adjustable( runWithReplace
                          , traverseIntMapWithKeyWithAdjust
                          , traverseDMapWithKeyWithAdjust
                          , traverseDMapWithKeyWithAdjustWithMove
                          )
              , MonadHold
              , MonadSample
              , NotReady( notReady
                        , notReadyUntil
                        )
              , PerformEvent( Performable
                            , performEvent
                            , performEvent_
                            )
              , PerformEventT
              )
import Reflex.GI.Gtk.Run.Class (MonadRunGtk( askRunGtk_
                                           , askRunGtkPromise
                                           , askMakeSynchronousFire
                                           )
                               )
import Reflex.Host.Class ( MonadReflexCreateTrigger
                         , MonadReflexHost
                         , MonadSubscribeEvent
                         , ReflexHost
                         )

data RunGtkEnv = RunGtkEnv
  { actionQueue :: TChan (IO ())
  , gtkThreadId :: ThreadId
  , waitEventThreadException :: STM SomeException
  }

newtype RunGtkT t m a = RunGtkT
  { unGtkT :: ReaderT RunGtkEnv m a
  }
  deriving ( Functor
           , Applicative
           , Monad
           , MonadTrans
           , MonadIO
           , MonadRef
           , MonadException
           , MonadFix
           )

deriving instance MonadSubscribeEvent t m => MonadSubscribeEvent t (RunGtkT t m)
deriving instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (RunGtkT t m)
deriving instance MonadReflexHost t m => MonadReflexHost t (RunGtkT t m)
deriving instance MonadSample t m => MonadSample t (RunGtkT t m)
deriving instance MonadHold t m => MonadHold t (RunGtkT t m)
deriving instance NotReady t m => NotReady t (RunGtkT t m)

instance Adjustable t m => Adjustable t (RunGtkT t m) where
  runWithReplace (RunGtkT a) e = RunGtkT $ runWithReplace a $ unGtkT <$> e
  traverseIntMapWithKeyWithAdjust f m a = RunGtkT $ traverseIntMapWithKeyWithAdjust f' m a
    where f' k v = unGtkT $ f k v
  traverseDMapWithKeyWithAdjust f m e =
    RunGtkT $ traverseDMapWithKeyWithAdjust (\k v -> unGtkT $ f k v) m e
  traverseDMapWithKeyWithAdjustWithMove f m e =
    RunGtkT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unGtkT $ f k v) m e

instance PerformEvent t m => PerformEvent t (RunGtkT t m) where
  type Performable (RunGtkT t m) = RunGtkT t (Performable m)
  performEvent = RunGtkT . performEvent . fmap unGtkT
  performEvent_ = RunGtkT . performEvent_ . fmap unGtkT

instance (MonadIO m) => MonadRunGtk (RunGtkT t m) where
  askRunGtk_ = do
    actionChan <- RunGtkT $ asks actionQueue
    gtkTId <- RunGtkT $ asks gtkThreadId
    pure $ \a -> do
      myTId <- myThreadId
      let execute = if myTId == gtkTId
                    then id
                    else scheduleAction actionChan
      execute $ void a
  askRunGtkPromise = do
    actionQueue <- RunGtkT $ asks actionQueue
    gtkTId <- RunGtkT $ asks gtkThreadId
    pure $ \a -> do
      myTId <- myThreadId
      if myTId == gtkTId
        then pure <$> a
        else do
        answerMVar <- newEmptyMVar
        scheduleAction actionQueue $ try @SomeException a >>= putMVar answerMVar
        pure $ readMVar answerMVar >>= either throwIO pure
  askMakeSynchronousFire = do
    actionChan <- RunGtkT $ asks actionQueue
    waitEventThreadException' <- RunGtkT $ asks waitEventThreadException
    pure $ \fireAsynchronously x -> do
      firedTVar <- newTVarIO False
      fireAsynchronously x $ atomically $ writeTVar firedTVar True
      let waitCompleted = do
            hasFired <- readTVar firedTVar
            if hasFired
              then pure ()
              else retry
      fix $ \loop -> join $ atomically $
        (pure () <$ waitCompleted)
        `orElse` ( do
                     gtkAction <- readTChan actionChan
                     pure $ runGtkAction gtkAction >> loop
                 )
        `orElse` (waitEventThreadException' >>= throwSTM)

scheduleAction :: TChan (IO ()) -> IO () -> IO ()
scheduleAction actionChan action =
  atomically (writeTChan actionChan action)
  >> void ( threadsAddIdle PRIORITY_HIGH_IDLE $
            SOURCE_REMOVE <$ runScheduledActions actionChan
          )

runScheduledActions :: TChan (IO ()) -> IO ()
runScheduledActions actionChan =
  atomically (tryReadTChan actionChan)
  >>= mapM_ (\gtkAction -> runGtkAction gtkAction >> runScheduledActions actionChan)

runGtkAction :: IO () -> IO ()
runGtkAction a = mask_ $ catch a (const $ pure () :: SomeException -> IO ())

runGtkT :: (MonadIO m)
        => RunGtkT t m a
        -> STM SomeException
        -> ThreadId
        -> m a
runGtkT (RunGtkT a) waitEventThreadException gtkThreadId = do
  actionQueue <- liftIO newTChanIO
  runReaderT a RunGtkEnv{..}

instance ( NotReady t m
         , ReflexHost t
         ) => NotReady t (PerformEventT t (RunGtkT t m)) where
  notReady = pure ()
  notReadyUntil _ = pure ()