{-# LANGUAGE LambdaCase #-} {-# LANGUAGE RecordWildCards #-} -- | A simple application architecture style inspired by PureScript's Pux -- framework. module GI.Gtk.Declarative.App.Simple ( App(..) , Transition(..) , runInWindow , run ) where import Control.Concurrent import Control.Monad import Data.Int import Data.Text (Text) import Data.Typeable import qualified GI.Gdk as Gdk import qualified GI.GLib.Constants as GLib import qualified GI.Gtk as Gtk import GI.Gtk.Declarative import GI.Gtk.Declarative.EventSource import Pipes import Pipes.Concurrent -- | Describes an state reducer application. data App state event = App { update :: state -> event -> Transition state event -- ^ The update function of an application reduces the current state and -- a new event to a 'Transition', which decides if and how to transition -- to the next state. , view :: state -> Widget event -- ^ The view renders a state value as a 'Widget', parameterized by the -- 'App's event type. , inputs :: [Producer event IO ()] -- ^ Inputs are pipes 'Producer's that feed events into the application. , initialState :: state -- ^ The initial state value of the state reduction loop. } -- | The result of applying the 'update' function, deciding if and how to -- transition to the next state. data Transition state event = -- Transition to the given state, and with an IO action that may return a -- new event. Transition state (IO (Maybe event)) -- | Exit the application. | Exit -- | Run an 'App' in a 'Gtk.Window' that has already been set up. This IO action -- will loop, so run it in a separate thread using 'forkIO' if you're calling -- it before the GTK main loop. runInWindow :: Typeable event => Gtk.Window -> App state event -> IO () runInWindow window App {..} = do let firstMarkup = view initialState nextEvent <- newEmptyMVar subscription <- runUI $ do widget' <- Gtk.toWidget =<< create firstMarkup Gtk.containerAdd window widget' Gtk.widgetShowAll window subscribe firstMarkup widget' (publishEvent nextEvent) void . forkIO $ runEffect (mergeProducers inputs >-> publishInputEvents nextEvent) loop firstMarkup nextEvent subscription initialState where loop oldMarkup nextEvent oldSubscription oldModel = do event <- takeMVar nextEvent case update oldModel event of Transition newModel action -> do let newMarkup = view newModel sub <- runUI (patchContainer window oldMarkup newMarkup nextEvent) >>= \case Just newSubscription -> cancel oldSubscription *> pure newSubscription Nothing -> pure oldSubscription -- Make sure the MVar is empty. void (tryTakeMVar nextEvent) -- If the action returned by the update function produced an event, then -- we write that as the nextEvent to use directly. void . forkIO $ action >>= maybe (return ()) (putMVar nextEvent) -- Finally, we loop. loop newMarkup nextEvent sub newModel Exit -> return () -- | Initialize GTK, set up a new window, and run the application in it. This -- is a convenience function. If you need more flexibility, you should use -- 'runInWindow' instead. run :: Typeable event => Text -- ^ Window title -> Maybe (Int32, Int32) -- ^ Optional window size -> App state event -- ^ Application to run -> IO () run title size app = do void $ Gtk.init Nothing window <- Gtk.windowNew Gtk.WindowTypeToplevel void (Gtk.onWidgetDestroy window Gtk.mainQuit) Gtk.windowSetTitle window title case size of Just (width, height) -> Gtk.windowResize window width height Nothing -> return () void . forkIO $ do runInWindow window app -- In case the run loop exits, quit the main GTK loop. Gtk.mainQuit Gtk.main patchContainer :: Typeable event => Gtk.Window -> Widget event -> Widget event -> MVar event -> IO (Maybe Subscription) patchContainer w o1 o2 nextEvent = case patch o1 o2 of Modify f -> Gtk.containerGetChildren w >>= \case [] -> return Nothing (c : _) -> do widget' <- Gtk.toWidget c f widget' Gtk.widgetShowAll w Just <$> subscribe o2 widget' (publishEvent nextEvent) Replace createNew -> do Gtk.containerForall w (Gtk.containerRemove w) newWidget <- createNew Gtk.containerAdd w newWidget Gtk.widgetShowAll w Just <$> subscribe o2 newWidget (publishEvent nextEvent) Keep -> return Nothing publishEvent :: MVar event -> event -> IO () publishEvent mvar = void . tryPutMVar mvar mergeProducers :: [Producer a IO ()] -> Producer a IO () mergeProducers producers = do (output, input) <- liftIO $ spawn unbounded _ <- liftIO $ mapM (fork output) producers fromInput input where fork :: Output a -> Producer a IO () -> IO () fork output producer = void $ forkIO $ do runEffect $ producer >-> toOutput output performGC publishInputEvents :: MVar event -> Consumer event IO () publishInputEvents nextEvent = forever (await >>= liftIO . (putMVar nextEvent)) runUI :: IO a -> IO a runUI f = do r <- newEmptyMVar void . Gdk.threadsAddIdle GLib.PRIORITY_DEFAULT $ do f >>= putMVar r return False takeMVar r