vty-ui-1.0: src/Graphics/Vty/Widgets/EventLoop.hs
{-# LANGUAGE DeriveDataTypeable, ExistentialQuantification #-}
-- |This module provides the main event loop functionality for this
-- library. All vty-ui applications must use runUi to get anything
-- done usefully.
module Graphics.Vty.Widgets.EventLoop
( Collection
, CollectionError(..)
, runUi
, schedule
, newCollection
, addToCollection
, setCurrentEntry
)
where
import Data.IORef
import Data.Typeable
import Control.Concurrent
import Control.Exception
import Control.Monad
import Control.Monad.Trans
import System.IO.Unsafe ( unsafePerformIO )
import Graphics.Vty
import Graphics.Vty.Widgets.Core
data CombinedEvent = VTYEvent Event
| UserEvent UserEvent
data UserEvent = ScheduledAction (IO ())
eventChan :: Chan CombinedEvent
{-# NOINLINE eventChan #-}
eventChan = unsafePerformIO newChan
-- |Run the main vty-ui event loop using the specified interface
-- collection and initial rendering context. The rendering context
-- provides the default attributes and 'Skin' to use for the
-- application. Throws 'BadCollectionIndex' if the specified
-- 'Collection' is empty.
runUi :: (MonadIO m) => Collection -> RenderContext -> m ()
runUi collection ctx =
liftIO $ do
vty <- mkVty
-- Create VTY event listener thread
_ <- forkIO $ vtyEventListener vty eventChan
runUi' vty eventChan collection ctx `finally` do
reserve_display $ terminal vty
shutdown vty
vtyEventListener :: Vty -> Chan CombinedEvent -> IO ()
vtyEventListener vty chan =
forever $ do
e <- next_event vty
writeChan chan $ VTYEvent e
-- |Schedule a widget-mutating 'IO' action to be run by the main event
-- loop. Use of this function is required to guarantee consistency
-- between interface presentation and internal state.
schedule :: (MonadIO m) => IO () -> m ()
schedule act = liftIO $ writeChan eventChan $ UserEvent $ ScheduledAction act
runUi' :: Vty -> Chan CombinedEvent -> Collection -> RenderContext -> IO ()
runUi' vty chan collection ctx = do
sz <- display_bounds $ terminal vty
e <- getCurrentEntry collection
let fg = entryFocusGroup e
img <- entryRenderAndPosition e (DisplayRegion 0 0) sz ctx
update vty $ pic_for_image img
mPos <- getCursorPosition fg
case mPos of
Just (DisplayRegion w h) -> do
show_cursor $ terminal vty
set_cursor_pos (terminal vty) w h
Nothing -> hide_cursor $ terminal vty
evt <- readChan chan
case evt of
VTYEvent (EvKey k mods) -> handleKeyEvent fg k mods >> return ()
UserEvent (ScheduledAction act) -> liftIO act
_ -> return ()
runUi' vty chan collection ctx
data CollectionError = BadCollectionIndex Int
deriving (Show, Typeable)
instance Exception CollectionError
data Entry = forall a. (Show a) => Entry (Widget a) (Widget FocusGroup)
data CollectionData =
CollectionData { entries :: [Entry]
, currentEntryNum :: Int
}
-- |The type of user interface collections.
type Collection = IORef CollectionData
instance Show CollectionData where
show (CollectionData es num) = concat [ "Collection { "
, "entries = <", show $ length es, "entries>"
, ", currentEntryNum = ", show num
, " }"
]
entryRenderAndPosition :: (MonadIO m) => Entry -> DisplayRegion -> DisplayRegion -> RenderContext -> m Image
entryRenderAndPosition (Entry w _) = renderAndPosition w
entryFocusGroup :: Entry -> Widget FocusGroup
entryFocusGroup (Entry _ fg) = fg
-- |Create a new collection.
newCollection :: (MonadIO m) => m Collection
newCollection =
liftIO $ newIORef $ CollectionData { entries = []
, currentEntryNum = -1
}
getCurrentEntry :: (MonadIO m) => Collection -> m Entry
getCurrentEntry cRef = do
cur <- currentEntryNum <~ cRef
es <- entries <~ cRef
if cur == -1 then
throw $ BadCollectionIndex cur else
if cur >= 0 && cur < length es then
return $ es !! cur else
throw $ BadCollectionIndex cur
-- |Add a widget and its focus group to a collection. Returns an
-- action which, when invoked, will switch to the interface specified
-- in the call.
addToCollection :: (MonadIO m, Show a) => Collection -> Widget a -> Widget FocusGroup -> m (m ())
addToCollection cRef wRef fg = do
i <- (length . entries) <~ cRef
liftIO $ modifyIORef cRef $ \st ->
st { entries = (entries st) ++ [Entry wRef fg]
, currentEntryNum = if currentEntryNum st == -1
then 0
else currentEntryNum st
}
resetFocusGroup fg
return $ setCurrentEntry cRef i
setCurrentEntry :: (MonadIO m) => Collection -> Int -> m ()
setCurrentEntry cRef i = do
st <- liftIO $ readIORef cRef
if i < length (entries st) && i >= 0 then
(liftIO $ modifyIORef cRef $ \s -> s { currentEntryNum = i }) else
throw $ BadCollectionIndex i
e <- getCurrentEntry cRef
resetFocusGroup $ entryFocusGroup e