Separate Widgets from Host. add splitVDrag

This commit is contained in:
Ali Abrar 2018-10-28 10:54:05 -04:00
parent 815c86ae42
commit ef1b495edc
5 changed files with 600 additions and 510 deletions

View File

@ -12,6 +12,8 @@ cabal-version: >=1.10
library
exposed-modules: Reflex.Vty
, Reflex.Vty.Host
, Reflex.Vty.Widget
other-modules: Reflex.Spider.Orphans
build-depends:
base,

View File

@ -14,6 +14,7 @@ import Data.Time
import qualified Graphics.Vty as V
import Reflex
import Reflex.Vty
import Reflex.Vty.Widget
{-
guest :: forall t m. VtyApp t m
@ -46,7 +47,7 @@ guest e = do
main :: IO ()
main =
mainVtyWidget $ do
mainWidget $ do
inp <- input
tellShutdown . fforMaybe inp $ \case
V.EvKey V.KEsc _ -> Just ()
@ -63,9 +64,14 @@ testBoxes = do
region2 = fmap (\(w,h) -> Region (w `div` 4) (h `div` 4) (2 * (w `div` 3)) (2*(h `div` 3))) size
pane region1 (constDyn False) . box $ debugInput
pane region2 (constDyn True) . box $
splitV (pure $ fractionSz 0.5) (pure (True, True)) (box debugInput) (box dragTest)
splitVDrag (box debugInput) (box dragTest)
return ()
debugFocus :: (Reflex t, Monad m) => VtyWidget t m ()
debugFocus = do
f <- focus
string $ show <$> current f
debugInput :: (Reflex t, MonadHold t m) => VtyWidget t m ()
debugInput = do
lastEvent <- hold "No event yet" . fmap show =<< input
@ -78,4 +84,4 @@ dragTest = do
testStringBox :: (Reflex t, Monad m) => VtyWidget t m ()
testStringBox = box .
string . pure . take 500 $ cycle ('\n' : ['a'..'z'])
string . pure . take 500 $ cycle ('\n' : ['a'..'z'])

View File

@ -1,510 +1,7 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE RecursiveDo #-}
{-# OPTIONS_GHC -Wall #-}
module Reflex.Vty
( VtyApp
, VtyResult(..)
, VtyEvent
, runVtyApp
, runVtyAppWith
, HasDisplaySize(..)
, displayWidth
, displayHeight
, HasFocus(..)
, HasVtyInput(..)
, Region(..)
, regionSize
, regionBlankImage
, boxImages
, runVtyWidget
, VtyWidgetCtx(..)
, VtyWidget(..)
, VtyWidgetOut(..)
, Drag (..)
, drag
, pane
, modifyImages
, mainVtyWidget
, filterInput
, tellImages
, tellShutdown
, wrapString
, splitV
, fractionSz
, box
, string
( module Reflex.Vty.Host
, module Reflex.Vty.Widget
) where
import Control.Applicative
import Control.Concurrent (forkIO, killThread)
import Control.Concurrent.Chan (newChan, readChan, writeChan)
import Control.Monad (forM, forM_, forever)
import Control.Monad.Fix (MonadFix, fix)
import Control.Monad.IO.Class (liftIO, MonadIO)
import Control.Monad.Identity (Identity(..))
import Control.Monad.Primitive (PrimMonad)
import Control.Monad.Ref (MonadRef, Ref, readRef)
import Control.Monad.Trans.Reader
import Control.Monad.Trans.Writer
import Control.Monad.Trans
import Data.Dependent.Sum (DSum ((:=>)))
import Data.IORef (IORef)
import Data.IORef (readIORef)
import Data.Maybe (catMaybes)
import Reflex
import Reflex.Host.Class
import Reflex.NotReady.Class
import Reflex.Spider.Orphans ()
import qualified Graphics.Vty as V
import Graphics.Vty (DisplayRegion, Image, Attr)
type VtyEvent = V.Event
-- | The output of a 'VtyApp'.
data VtyResult t = VtyResult
{ _vtyResult_picture :: Behavior t V.Picture
-- ^ The current vty output. 'runVtyAppWith' samples this value every time an
-- event fires and updates the display.
, _vtyResult_shutdown :: Event t ()
-- ^ An event that requests application termination.
}
type MonadVtyApp t m =
( Reflex t
, MonadHold t m
, MonadFix m
, PrimMonad (HostFrame t)
, ReflexHost t
, MonadIO (HostFrame t)
, Ref m ~ IORef
, Ref (HostFrame t) ~ IORef
, MonadRef (HostFrame t)
, NotReady t m
, TriggerEvent t m
, PostBuild t m
, PerformEvent t m
, MonadIO m
, MonadIO (Performable m)
)
-- | A functional reactive vty application.
type VtyApp t m = MonadVtyApp t m
=> DisplayRegion
-- ^ The initial display size (updates to this come as events)
-> Event t (V.Event)
-- ^ Vty input events.
-> m (VtyResult t)
-- ^ The output of the 'VtyApp'. The application runs in a context that,
-- among other things, allows new events to be created and triggered
-- ('TriggerEvent'), provides access to an event that fires immediately upon
-- app instantiation ('PostBuild'), and allows actions to be run upon
-- occurrences of events ('PerformEvent').
-- | Runs a 'VtyApp' in a given 'Vty'.
runVtyAppWith
:: V.Vty
-- ^ A 'Vty' handle.
-> (forall t m. VtyApp t m)
-- ^ A functional reactive vty application.
-> IO ()
runVtyAppWith vty vtyGuest =
-- We are using the 'Spider' implementation of reflex. Running the host
-- allows us to take actions on the FRP timeline. The scoped type signature
-- specifies that our host runs on the Global timeline.
-- For more information, see 'Reflex.Spider.Internal.runSpiderHost'.
(runSpiderHost :: SpiderHost Global a -> IO a) $ do
-- Create an 'Event' and a "trigger" reference for that event. The trigger
-- reference can be used to determine whether anyone is "subscribed" to
-- that 'Event' and, therefore, whether we need to bother performing any
-- updates when the 'Event' fires.
-- The 'Event' below will be used to convey vty input events.
(vtyEvent, vtyEventTriggerRef) <- newEventWithTriggerRef
-- Create the "post-build" event and associated trigger. This event fires
-- once, when the application starts.
(postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
-- Create a queue to which we will write 'Event's that need to be
-- processed.
events <- liftIO newChan
displayRegion0 <- V.displayBounds $ V.outputIface vty
-- Run the vty "guest" application, providing the appropriate context. The
-- result is a 'VtyResult', and a 'FireCommand' that will be used to
-- trigger events.
(vtyResult, fc@(FireCommand fire)) <- do
hostPerformEventT $ -- Allows the guest app to run
-- 'performEvent', so that actions
-- (e.g., IO actions) can be run when
-- 'Event's fire.
flip runPostBuildT postBuild $ -- Allows the guest app to access to
-- a "post-build" 'Event'
flip runTriggerEventT events $ -- Allows the guest app to create new
-- events and triggers and writes
-- those triggers to a channel from
-- which they will be read and
-- processed.
vtyGuest displayRegion0 vtyEvent
-- The guest app is provided the
-- initial display region and an
-- 'Event' of vty inputs.
-- Reads the current value of the 'Picture' behavior and updates the
-- display with it. This will be called whenever we determine that a
-- display update is necessary. In this implementation that is when various
-- events occur.
let updateVty =
sample (_vtyResult_picture vtyResult) >>= liftIO . V.update vty
-- Read the trigger reference for the post-build event. This will be
-- 'Nothing' if the guest application hasn't subscribed to this event.
mPostBuildTrigger <- readRef postBuildTriggerRef
-- When there is a subscriber to the post-build event, fire the event.
forM_ mPostBuildTrigger $ \postBuildTrigger ->
fire [postBuildTrigger :=> Identity ()] $ return ()
-- After firing the post-build event, sample the vty result and update
-- the display.
updateVty
-- Subscribe to an 'Event' of that the guest application can use to
-- request application shutdown. We'll check whether this 'Event' is firing
-- to determine whether to terminate.
shutdown <- subscribeEvent $ _vtyResult_shutdown vtyResult
-- Fork a thread and continuously get the next vty input event, and then
-- write the input event to our channel of FRP 'Event' triggers.
-- The thread is forked here because 'nextEvent' blocks.
nextEventThread <- liftIO $ forkIO $ forever $ do
-- Retrieve the next input event.
ne <- V.nextEvent vty
let -- The reference to the vty input 'EventTrigger'. This is the trigger
-- we'd like to associate the input event value with.
triggerRef = EventTriggerRef vtyEventTriggerRef
-- Create an event 'TriggerInvocation' with the value that we'd like
-- the event to have if it is fired. It may not fire with this value
-- if nobody is subscribed to the 'Event'.
triggerInvocation = TriggerInvocation ne $ return ()
-- Write our input event's 'EventTrigger' with the newly created
-- 'TriggerInvocation' value to the queue of events.
writeChan events $ [triggerRef :=> triggerInvocation]
-- The main application loop. We wait for new events, fire those that
-- have subscribers, and update the display. If we detect a shutdown
-- request, the application terminates.
fix $ \loop -> do
-- Read the next event (blocking).
ers <- liftIO $ readChan events
stop <- do
-- Fire events that have subscribers.
fireEventTriggerRefs fc ers $
-- Check if the shutdown 'Event' is firing.
readEvent shutdown >>= \case
Nothing -> return False
Just _ -> return True
if or stop
then liftIO $ do -- If we received a shutdown 'Event'
killThread nextEventThread -- then stop reading input events and
V.shutdown vty -- call the 'Vty's shutdown command.
else do -- Otherwise, update the display and loop.
updateVty
loop
where
-- TODO Some part of this is probably general enough to belong in reflex
-- | Use the given 'FireCommand' to fire events that have subscribers
-- and call the callback for the 'TriggerInvocation' of each.
fireEventTriggerRefs
:: (Monad (ReadPhase m), MonadIO m)
=> FireCommand t m
-> [DSum (EventTriggerRef t) TriggerInvocation]
-> ReadPhase m a
-> m [a]
fireEventTriggerRefs (FireCommand fire) ers rcb = do
mes <- liftIO $
forM ers $ \(EventTriggerRef er :=> TriggerInvocation a _) -> do
me <- readIORef er
return $ fmap (\e -> e :=> Identity a) me
a <- fire (catMaybes mes) rcb
liftIO $ forM_ ers $ \(_ :=> TriggerInvocation _ cb) -> cb
return a
-- | Run a 'VtyApp' with a 'Vty' handle with a standard configuration.
runVtyApp
:: (forall t m. VtyApp t m)
-> IO ()
runVtyApp app = do
cfg <- liftIO V.standardIOConfig
vty <- liftIO $ V.mkVty $ cfg { V.mouseMode = Just True }
runVtyAppWith vty app
---- Widget.hs
data Region = Region
{ _region_left :: Int
, _region_top :: Int
, _region_width :: Int
, _region_height :: Int
}
deriving (Show, Read, Eq, Ord)
regionSize :: Region -> (Int, Int)
regionSize (Region _ _ w h) = (w, h)
regionBlankImage :: Region -> Image
regionBlankImage r@(Region _ _ width height) =
withinImage r $ wrapString width V.defAttr $ replicate (width * height) ' '
withinImage :: Region -> Image -> Image
withinImage (Region left top width height)
| width < 0 || height < 0 = withinImage (Region left top 0 0)
| otherwise = V.translate left top . V.crop width height
wrapString :: Int -> Attr -> String -> Image
wrapString maxWidth attrs = V.vertCat . concatMap (fmap (V.string attrs) . fmap (take maxWidth) . takeWhile (not . null) . iterate (drop maxWidth)) . lines
boxImages :: Region -> [Image]
boxImages r@(Region left top width height) =
let hBorder = V.string mempty $ replicate width '-'
vBorder = wrapString 1 mempty $ replicate (height - 2) '|'
in [ withinImage (r { _region_height = 1 }) hBorder
, withinImage (Region left (top + 1) 1 (height - 2)) vBorder
, withinImage (Region (left + width - 1) (top + 1) 1 (height - 2)) vBorder
, withinImage (r { _region_top = top + height - 1 }) hBorder
]
class (Reflex t, Monad m) => HasDisplaySize t m | m -> t where
displaySize :: m (Dynamic t (Int, Int))
displayWidth :: HasDisplaySize t m => m (Dynamic t Int)
displayWidth = fmap fst <$> displaySize
displayHeight :: HasDisplaySize t m => m (Dynamic t Int)
displayHeight = fmap snd <$> displaySize
class HasVtyInput t m | m -> t where
input :: m (Event t VtyEvent)
class HasFocus t m | m -> t where
focus :: m (Dynamic t Bool)
data VtyWidgetCtx t = VtyWidgetCtx
{ _vtyWidgetCtx_size :: Dynamic t (Int,Int) -- ^ The width and height of the region allocated to the widget.
, _vtyWidgetCtx_focus :: Dynamic t Bool -- ^ Whether the widget should behave as if it has focus for keyboard input.
, _vtyWidgetCtx_input :: Event t VtyEvent -- ^ User input events that the widget's parent chooses to share. These will generally
-- be filtered for relevance:
-- * Keyboard inputs are restricted to focused widgets
-- * Mouse inputs are restricted to the region in which the widget resides.
}
data VtyWidgetOut t = VtyWidgetOut
{ _vtyWidgetOut_images :: Behavior t [Image]
, _vtyWidgetOut_shutdown :: Event t ()
}
instance (Reflex t) => Semigroup (VtyWidgetOut t) where
wo <> wo' = VtyWidgetOut
{ _vtyWidgetOut_images = _vtyWidgetOut_images wo <> _vtyWidgetOut_images wo'
, _vtyWidgetOut_shutdown = _vtyWidgetOut_shutdown wo <> _vtyWidgetOut_shutdown wo'
}
instance (Reflex t) => Monoid (VtyWidgetOut t) where
mempty = VtyWidgetOut mempty mempty
mappend wo wo' = wo <> wo'
newtype VtyWidget t m a = VtyWidget { unVtyWidget :: WriterT (VtyWidgetOut t) (ReaderT (VtyWidgetCtx t) m) a }
deriving (Functor, Applicative, Monad, MonadSample t, MonadHold t, MonadFix)
runVtyWidget :: (Reflex t)
=> VtyWidgetCtx t
-> VtyWidget t m a
-> m (a, VtyWidgetOut t)
runVtyWidget ctx w = runReaderT (runWriterT (unVtyWidget w)) ctx
mainVtyWidget :: (forall t m. MonadVtyApp t m => VtyWidget t m ()) -> IO ()
mainVtyWidget child =
runVtyApp $ \dr0 inp -> do
size <- holdDyn dr0 $ fforMaybe inp $ \case
V.EvResize w h -> Just (w, h)
_ -> Nothing
let ctx = VtyWidgetCtx
{ _vtyWidgetCtx_size = size
, _vtyWidgetCtx_input = inp
, _vtyWidgetCtx_focus = constDyn True
}
((), wo) <- runVtyWidget ctx child
return $ VtyResult
{ _vtyResult_picture = fmap (V.picForLayers . reverse) (_vtyWidgetOut_images wo)
, _vtyResult_shutdown = _vtyWidgetOut_shutdown wo
}
modifyImages :: (Reflex t, MonadHold t m, MonadFix m)
=> Behavior t ([Image] -> [Image]) -> VtyWidget t m a -> VtyWidget t m a
modifyImages f (VtyWidget w) = VtyWidget $
censor (\wo -> wo { _vtyWidgetOut_images = f <*> (_vtyWidgetOut_images wo) })
w
instance (Reflex t, Monad m) => HasDisplaySize t (VtyWidget t m) where
displaySize = VtyWidget . lift $ asks _vtyWidgetCtx_size
instance (Reflex t, Monad m) => HasVtyInput t (VtyWidget t m) where
input = VtyWidget . lift $ asks _vtyWidgetCtx_input
instance (Reflex t, Monad m) => HasFocus t (VtyWidget t m) where
focus = VtyWidget . lift $ asks _vtyWidgetCtx_focus
tellImages :: (Reflex t, Monad m) => Behavior t [Image] -> VtyWidget t m ()
tellImages imgs = VtyWidget $ tell (mempty { _vtyWidgetOut_images = imgs })
tellShutdown :: (Reflex t, Monad m) => Event t () -> VtyWidget t m ()
tellShutdown sd = VtyWidget $ tell (mempty { _vtyWidgetOut_shutdown = sd })
data Drag = Drag
{ _drag_from :: (Int, Int) -- ^ Where the drag began
, _drag_to :: (Int, Int) -- ^ Where the mouse currently is
, _drag_button :: V.Button -- ^ Which mouse button is dragging
, _drag_modifiers :: [V.Modifier] -- ^ What modifiers are held
, _drag_end :: Bool -- ^ Whether the drag ended (the mouse button was released)
}
deriving (Eq, Ord, Show)
drag :: (Reflex t, MonadFix m, MonadHold t m) => V.Button -> VtyWidget t m (Event t Drag)
drag btn = do
inp <- input
let f :: Drag -> V.Event -> Maybe Drag
f (Drag from _ _ mods end) = \case
V.EvMouseDown x y btn' mods'
| end -> Just $ Drag (x,y) (x,y) btn' mods' False
| btn == btn' -> Just $ Drag from (x,y) btn mods' False
| otherwise -> Nothing -- Ignore other buttons.
V.EvMouseUp x y (Just btn')
| end -> Nothing
| btn == btn' -> Just $ Drag from (x,y) btn mods True
| otherwise -> Nothing
V.EvMouseUp x y Nothing -- Terminal doesn't specify mouse up button,
-- assume it's the right one.
| end -> Nothing
| otherwise -> Just $ Drag from (x,y) btn mods True
_ -> Nothing
rec let newDrag = attachWithMaybe f (current dragD) inp
dragD <- holdDyn (Drag (0,0) (0,0) V.BLeft [] True) -- gross, but ok.
newDrag
return (updated dragD)
pane :: (Reflex t, Monad m)
=> Dynamic t Region -- ^ Region into which we should draw the widget (in coordinates relative to our own)
-> Dynamic t Bool -- ^ Whether the widget should be focused when the parent is.
-> VtyWidget t m a
-> VtyWidget t m a
pane reg foc child = VtyWidget $ do
ctx <- lift ask
let ctx' = VtyWidgetCtx
{ _vtyWidgetCtx_input = leftmost -- TODO: think about this leftmost more.
[ ffor (updated reg) $ \(Region _ _ w h) -> V.EvResize w h
, fmapMaybe id $
attachWith (\(r,f) e -> filterInput r f e)
(liftA2 (,) (current reg) (current foc))
(_vtyWidgetCtx_input ctx)
]
, _vtyWidgetCtx_focus = liftA2 (&&) (_vtyWidgetCtx_focus ctx) foc
, _vtyWidgetCtx_size = fmap regionSize reg }
(result, wo) <- lift . lift $ runVtyWidget ctx' child
let images = _vtyWidgetOut_images wo
images' = liftA2 (\r is -> map (withinImage r) is) (current reg) images
wo' = wo { _vtyWidgetOut_images = images' }
tell wo'
return result
filterInput :: Region -> Bool -> VtyEvent -> Maybe VtyEvent
filterInput (Region l t w h) focused e = case e of
V.EvKey _ _ | not focused -> Nothing
V.EvMouseDown x y btn m -> mouse (\u v -> V.EvMouseDown u v btn m) x y
V.EvMouseUp x y btn -> mouse (\u v -> V.EvMouseUp u v btn) x y
_ -> Just e
where
mouse con x y
| or [ x < l
, y < t
, x >= l + w
, y >= t + h ] = Nothing
| otherwise =
Just (con (x - l) (y - t))
-- | A plain split of the available space into vertically stacked panes.
-- No visual separator is built in here.
splitV :: (Reflex t, Monad m)
=> Dynamic t (Int -> Int)
-- ^ Function used to determine size of first pane based on available size
-> Dynamic t (Bool, Bool)
-- ^ How to focus the two sub-panes, given that we are focused.
-> VtyWidget t m a
-- ^ Widget for first pane
-> VtyWidget t m b
-- ^ Widget for second pane
-> VtyWidget t m (a,b)
splitV sizeFunD focD wA wB = do
sz <- displaySize
let regA = (\f (w,h) -> Region 0 0 w (f h)) <$> sizeFunD <*> sz
regB = (\(w,h) (Region _ _ _ hA) -> Region 0 hA w (h - hA)) <$> sz <*> regA
ra <- pane regA (fst <$> focD) wA
rb <- pane regB (snd <$> focD) wB
return (ra,rb)
-- | A split of the available space into two parts with a draggable separator.
-- Starts with half the space allocated to each, and the first pane has focus.
-- Clicking in a pane switches focus.
{-
splitVDrag :: (Reflex t, Monad m)
=> VtyWidget t m a
-> VtyWidget t m b
-> VtyWidget t m (a,b)
splitVDrag wA wB = do
sz <- displaySize
let splitterPos = ffor sz $ \(_,h) -> h `div` 2
regA = (\(w,_) sp -> Region 0 0 w sp) <$> sz <*> splitterPos
regS = (\(w,h) sp -> Region 0 sp w 1) <$> sz <*> splitterPos
regB = (\(w,h) sp -> Region 0 (sp + 1) w (h - sp - 1)) <$> sz <*> splitterPos
foc <- holdDyn False []
(rA, inpA) <- pane wA
-}
fractionSz :: Double -> Int -> Int
fractionSz x h = round (fromIntegral h * x)
box :: (Monad m, Reflex t)
=> VtyWidget t m a
-> VtyWidget t m a
box child = do
sz <- displaySize
let boxReg = ffor (current sz) $ \(w,h) -> Region 0 0 w h
innerReg = ffor sz $ \(w,h) -> Region 1 1 (w - 2) (h - 2)
tellImages (fmap boxImages boxReg)
tellImages (fmap (\r -> [regionBlankImage r]) (current innerReg))
pane innerReg (pure True) child
string :: (Reflex t, Monad m) => Behavior t String -> VtyWidget t m ()
string msg = do
dw <- displayWidth
let img = (\w s -> [wrapString w mempty s]) <$> current dw <*> msg
tellImages img
import Reflex.Vty.Host
import Reflex.Vty.Widget

231
src/Reflex/Vty/Host.hs Normal file
View File

@ -0,0 +1,231 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE RecursiveDo #-}
module Reflex.Vty.Host
( VtyApp
, VtyResult(..)
, getDefaultVty
, runVtyApp
, runVtyAppWithHandle
, MonadVtyApp
, VtyEvent
) where
import Control.Concurrent (forkIO, killThread)
import Control.Concurrent.Chan (newChan, readChan, writeChan)
import Control.Monad (forM, forM_, forever)
import Control.Monad.Fix (MonadFix, fix)
import Control.Monad.IO.Class (liftIO, MonadIO)
import Control.Monad.Identity (Identity(..))
import Control.Monad.Primitive (PrimMonad)
import Control.Monad.Ref (MonadRef, Ref, readRef)
import Data.Dependent.Sum (DSum ((:=>)))
import Data.IORef (IORef)
import Data.IORef (readIORef)
import Data.Maybe (catMaybes)
import Reflex
import Reflex.Host.Class
import Reflex.NotReady.Class
import Reflex.Spider.Orphans ()
import qualified Graphics.Vty as V
import Graphics.Vty (DisplayRegion)
type VtyEvent = V.Event
-- | The output of a 'VtyApp'.
data VtyResult t = VtyResult
{ _vtyResult_picture :: Behavior t V.Picture
-- ^ The current vty output. 'runVtyAppWith' samples this value every time an
-- event fires and updates the display.
, _vtyResult_shutdown :: Event t ()
-- ^ An event that requests application termination.
}
type MonadVtyApp t m =
( Reflex t
, MonadHold t m
, MonadFix m
, PrimMonad (HostFrame t)
, ReflexHost t
, MonadIO (HostFrame t)
, Ref m ~ IORef
, Ref (HostFrame t) ~ IORef
, MonadRef (HostFrame t)
, NotReady t m
, TriggerEvent t m
, PostBuild t m
, PerformEvent t m
, MonadIO m
, MonadIO (Performable m)
)
-- | A functional reactive vty application.
type VtyApp t m = MonadVtyApp t m
=> DisplayRegion
-- ^ The initial display size (updates to this come as events)
-> Event t (V.Event)
-- ^ Vty input events.
-> m (VtyResult t)
-- ^ The output of the 'VtyApp'. The application runs in a context that,
-- among other things, allows new events to be created and triggered
-- ('TriggerEvent'), provides access to an event that fires immediately upon
-- app instantiation ('PostBuild'), and allows actions to be run upon
-- occurrences of events ('PerformEvent').
-- | Runs a 'VtyApp' in a given 'Vty'.
runVtyAppWithHandle
:: V.Vty
-- ^ A 'Vty' handle.
-> (forall t m. VtyApp t m)
-- ^ A functional reactive vty application.
-> IO ()
runVtyAppWithHandle vty vtyGuest =
-- We are using the 'Spider' implementation of reflex. Running the host
-- allows us to take actions on the FRP timeline. The scoped type signature
-- specifies that our host runs on the Global timeline.
-- For more information, see 'Reflex.Spider.Internal.runSpiderHost'.
(runSpiderHost :: SpiderHost Global a -> IO a) $ do
-- Create an 'Event' and a "trigger" reference for that event. The trigger
-- reference can be used to determine whether anyone is "subscribed" to
-- that 'Event' and, therefore, whether we need to bother performing any
-- updates when the 'Event' fires.
-- The 'Event' below will be used to convey vty input events.
(vtyEvent, vtyEventTriggerRef) <- newEventWithTriggerRef
-- Create the "post-build" event and associated trigger. This event fires
-- once, when the application starts.
(postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
-- Create a queue to which we will write 'Event's that need to be
-- processed.
events <- liftIO newChan
displayRegion0 <- V.displayBounds $ V.outputIface vty
-- Run the vty "guest" application, providing the appropriate context. The
-- result is a 'VtyResult', and a 'FireCommand' that will be used to
-- trigger events.
(vtyResult, fc@(FireCommand fire)) <- do
hostPerformEventT $ -- Allows the guest app to run
-- 'performEvent', so that actions
-- (e.g., IO actions) can be run when
-- 'Event's fire.
flip runPostBuildT postBuild $ -- Allows the guest app to access to
-- a "post-build" 'Event'
flip runTriggerEventT events $ -- Allows the guest app to create new
-- events and triggers and writes
-- those triggers to a channel from
-- which they will be read and
-- processed.
vtyGuest displayRegion0 vtyEvent
-- The guest app is provided the
-- initial display region and an
-- 'Event' of vty inputs.
-- Reads the current value of the 'Picture' behavior and updates the
-- display with it. This will be called whenever we determine that a
-- display update is necessary. In this implementation that is when various
-- events occur.
let updateVty =
sample (_vtyResult_picture vtyResult) >>= liftIO . V.update vty
-- Read the trigger reference for the post-build event. This will be
-- 'Nothing' if the guest application hasn't subscribed to this event.
mPostBuildTrigger <- readRef postBuildTriggerRef
-- When there is a subscriber to the post-build event, fire the event.
forM_ mPostBuildTrigger $ \postBuildTrigger ->
fire [postBuildTrigger :=> Identity ()] $ return ()
-- After firing the post-build event, sample the vty result and update
-- the display.
updateVty
-- Subscribe to an 'Event' of that the guest application can use to
-- request application shutdown. We'll check whether this 'Event' is firing
-- to determine whether to terminate.
shutdown <- subscribeEvent $ _vtyResult_shutdown vtyResult
-- Fork a thread and continuously get the next vty input event, and then
-- write the input event to our channel of FRP 'Event' triggers.
-- The thread is forked here because 'nextEvent' blocks.
nextEventThread <- liftIO $ forkIO $ forever $ do
-- Retrieve the next input event.
ne <- V.nextEvent vty
let -- The reference to the vty input 'EventTrigger'. This is the trigger
-- we'd like to associate the input event value with.
triggerRef = EventTriggerRef vtyEventTriggerRef
-- Create an event 'TriggerInvocation' with the value that we'd like
-- the event to have if it is fired. It may not fire with this value
-- if nobody is subscribed to the 'Event'.
triggerInvocation = TriggerInvocation ne $ return ()
-- Write our input event's 'EventTrigger' with the newly created
-- 'TriggerInvocation' value to the queue of events.
writeChan events $ [triggerRef :=> triggerInvocation]
-- The main application loop. We wait for new events, fire those that
-- have subscribers, and update the display. If we detect a shutdown
-- request, the application terminates.
fix $ \loop -> do
-- Read the next event (blocking).
ers <- liftIO $ readChan events
stop <- do
-- Fire events that have subscribers.
fireEventTriggerRefs fc ers $
-- Check if the shutdown 'Event' is firing.
readEvent shutdown >>= \case
Nothing -> return False
Just _ -> return True
if or stop
then liftIO $ do -- If we received a shutdown 'Event'
killThread nextEventThread -- then stop reading input events and
V.shutdown vty -- call the 'Vty's shutdown command.
else do -- Otherwise, update the display and loop.
updateVty
loop
where
-- TODO Some part of this is probably general enough to belong in reflex
-- | Use the given 'FireCommand' to fire events that have subscribers
-- and call the callback for the 'TriggerInvocation' of each.
fireEventTriggerRefs
:: (Monad (ReadPhase m), MonadIO m)
=> FireCommand t m
-> [DSum (EventTriggerRef t) TriggerInvocation]
-> ReadPhase m a
-> m [a]
fireEventTriggerRefs (FireCommand fire) ers rcb = do
mes <- liftIO $
forM ers $ \(EventTriggerRef er :=> TriggerInvocation a _) -> do
me <- readIORef er
return $ fmap (\e -> e :=> Identity a) me
a <- fire (catMaybes mes) rcb
liftIO $ forM_ ers $ \(_ :=> TriggerInvocation _ cb) -> cb
return a
-- | Run a 'VtyApp' with a 'Vty' handle with a standard configuration.
runVtyApp
:: (forall t m. VtyApp t m)
-> IO ()
runVtyApp app = do
vty <- getDefaultVty
runVtyAppWithHandle vty app
getDefaultVty :: IO V.Vty
getDefaultVty = do
cfg <- V.standardIOConfig
V.mkVty $ cfg { V.mouseMode = Just True }

354
src/Reflex/Vty/Widget.hs Normal file
View File

@ -0,0 +1,354 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE TypeFamilies #-}
module Reflex.Vty.Widget
( VtyWidgetCtx(..)
, VtyWidget(..)
, VtyWidgetOut(..)
, runVtyWidget
, mainWidget
, mainWidgetWithHandle
, HasDisplaySize(..)
, displayWidth
, displayHeight
, HasFocus(..)
, HasVtyInput(..)
, Region(..)
, regionSize
, regionBlankImage
, Drag (..)
, drag
, mouseDown
, pane
, modifyImages
, tellImages
, tellShutdown
, wrapString
, splitV
, splitVDrag
, fractionSz
, box
, string
) where
import Control.Applicative (liftA2)
import Control.Monad.Fix (MonadFix)
import Control.Monad.Trans (lift)
import Control.Monad.Trans.Reader (ReaderT, runReaderT, asks, ask)
import Control.Monad.Trans.Writer (WriterT, runWriterT, censor, tell)
import Graphics.Vty (Image, Attr)
import qualified Graphics.Vty as V
import Reflex
import Reflex.Vty.Host
-- | The context within which a 'VtyWidget' runs
data VtyWidgetCtx t = VtyWidgetCtx
{ _vtyWidgetCtx_size :: Dynamic t (Int,Int)
-- ^ The width and height of the region allocated to the widget.
, _vtyWidgetCtx_focus :: Dynamic t Bool
-- ^ Whether the widget should behave as if it has focus for keyboard input.
, _vtyWidgetCtx_input :: Event t VtyEvent
-- ^ User input events that the widget's parent chooses to share. These will generally
-- be filtered for relevance:
-- * Keyboard inputs are restricted to focused widgets
-- * Mouse inputs are restricted to the region in which the widget resides and are
-- translated into its internal coordinates.
}
-- | The output of a 'VtyWidget'
data VtyWidgetOut t = VtyWidgetOut
{ _vtyWidgetOut_images :: Behavior t [Image]
-- ^ The visual output of the 'VtyWidget'
, _vtyWidgetOut_shutdown :: Event t ()
}
instance Reflex t => Semigroup (VtyWidgetOut t) where
wo <> wo' = VtyWidgetOut
{ _vtyWidgetOut_images = _vtyWidgetOut_images wo <> _vtyWidgetOut_images wo'
, _vtyWidgetOut_shutdown = _vtyWidgetOut_shutdown wo <> _vtyWidgetOut_shutdown wo'
}
instance (Reflex t) => Monoid (VtyWidgetOut t) where
mempty = VtyWidgetOut mempty mempty
mappend wo wo' = wo <> wo'
newtype VtyWidget t m a = VtyWidget { unVtyWidget :: WriterT (VtyWidgetOut t) (ReaderT (VtyWidgetCtx t) m) a }
deriving (Functor, Applicative, Monad, MonadSample t, MonadHold t, MonadFix)
-- | Runs a 'VtyWidget' with a given context
runVtyWidget :: (Reflex t)
=> VtyWidgetCtx t
-> VtyWidget t m a
-> m (a, VtyWidgetOut t)
runVtyWidget ctx w = runReaderT (runWriterT (unVtyWidget w)) ctx
-- | Sets up the top-level context for a 'VtyWidget' and runs it with that context
mainWidgetWithHandle :: V.Vty -> (forall t m. MonadVtyApp t m => VtyWidget t m ()) -> IO ()
mainWidgetWithHandle vty child =
runVtyAppWithHandle vty $ \dr0 inp -> do
size <- holdDyn dr0 $ fforMaybe inp $ \case
V.EvResize w h -> Just (w, h)
_ -> Nothing
let ctx = VtyWidgetCtx
{ _vtyWidgetCtx_size = size
, _vtyWidgetCtx_input = inp
, _vtyWidgetCtx_focus = constDyn True
}
((), wo) <- runVtyWidget ctx child
return $ VtyResult
{ _vtyResult_picture = fmap (V.picForLayers . reverse) (_vtyWidgetOut_images wo)
, _vtyResult_shutdown = _vtyWidgetOut_shutdown wo
}
-- | Like 'mainWidgetWithHandle', but uses a default vty configuration
mainWidget :: (forall t m. MonadVtyApp t m => VtyWidget t m ()) -> IO ()
mainWidget child = do
vty <- getDefaultVty
mainWidgetWithHandle vty child
class (Reflex t, Monad m) => HasDisplaySize t m | m -> t where
displaySize :: m (Dynamic t (Int, Int))
instance (Reflex t, Monad m) => HasDisplaySize t (VtyWidget t m) where
displaySize = VtyWidget . lift $ asks _vtyWidgetCtx_size
displayWidth :: HasDisplaySize t m => m (Dynamic t Int)
displayWidth = fmap fst <$> displaySize
displayHeight :: HasDisplaySize t m => m (Dynamic t Int)
displayHeight = fmap snd <$> displaySize
class HasVtyInput t m | m -> t where
input :: m (Event t VtyEvent)
instance (Reflex t, Monad m) => HasVtyInput t (VtyWidget t m) where
input = VtyWidget . lift $ asks _vtyWidgetCtx_input
class HasFocus t m | m -> t where
focus :: m (Dynamic t Bool)
instance (Reflex t, Monad m) => HasFocus t (VtyWidget t m) where
focus = VtyWidget . lift $ asks _vtyWidgetCtx_focus
class (Reflex t, Monad m) => ImageWriter t m | m -> t where
tellImages :: Behavior t [Image] -> m ()
instance (Reflex t, Monad m) => ImageWriter t (VtyWidget t m) where
tellImages imgs = VtyWidget $ tell (mempty { _vtyWidgetOut_images = imgs })
class (Reflex t, Monad m) => Shutdown t m where
tellShutdown :: Event t () -> m ()
instance (Reflex t, Monad m) => Shutdown t (VtyWidget t m) where
tellShutdown sd = VtyWidget $ tell (mempty { _vtyWidgetOut_shutdown = sd })
data Region = Region
{ _region_left :: Int
, _region_top :: Int
, _region_width :: Int
, _region_height :: Int
}
deriving (Show, Read, Eq, Ord)
regionSize :: Region -> (Int, Int)
regionSize (Region _ _ w h) = (w, h)
pane
:: (Reflex t, Monad m)
=> Dynamic t Region -- ^ Region into which we should draw the widget (in coordinates relative to our own)
-> Dynamic t Bool -- ^ Whether the widget should be focused when the parent is.
-> VtyWidget t m a
-> VtyWidget t m a
pane reg foc child = VtyWidget $ do
ctx <- lift ask
let ctx' = VtyWidgetCtx
{ _vtyWidgetCtx_input = leftmost -- TODO: think about this leftmost more.
[ ffor (updated reg) $ \(Region _ _ w h) -> V.EvResize w h
, fmapMaybe id $
attachWith (\(r,f) e -> filterInput r f e)
(liftA2 (,) (current reg) (current foc))
(_vtyWidgetCtx_input ctx)
]
, _vtyWidgetCtx_focus = liftA2 (&&) (_vtyWidgetCtx_focus ctx) foc
, _vtyWidgetCtx_size = fmap regionSize reg }
(result, wo) <- lift . lift $ runVtyWidget ctx' child
let images = _vtyWidgetOut_images wo
images' = liftA2 (\r is -> map (withinImage r) is) (current reg) images
wo' = wo { _vtyWidgetOut_images = images' }
tell wo'
return result
where
-- Filters input such that:
-- * unfocused widgets receive no key events
-- * mouse inputs outside the region are ignored
-- * mouse inputs inside the region have their coordinates translated
-- such that (0,0) is the top-left corner of the region
filterInput :: Region -> Bool -> VtyEvent -> Maybe VtyEvent
filterInput (Region l t w h) focused e = case e of
V.EvKey _ _ | not focused -> Nothing
V.EvMouseDown x y btn m -> mouse (\u v -> V.EvMouseDown u v btn m) x y
V.EvMouseUp x y btn -> mouse (\u v -> V.EvMouseUp u v btn) x y
_ -> Just e
where
mouse con x y
| or [ x < l
, y < t
, x >= l + w
, y >= t + h ] = Nothing
| otherwise =
Just (con (x - l) (y - t))
data Drag = Drag
{ _drag_from :: (Int, Int) -- ^ Where the drag began
, _drag_to :: (Int, Int) -- ^ Where the mouse currently is
, _drag_button :: V.Button -- ^ Which mouse button is dragging
, _drag_modifiers :: [V.Modifier] -- ^ What modifiers are held
, _drag_end :: Bool -- ^ Whether the drag ended (the mouse button was released)
}
deriving (Eq, Ord, Show)
drag
:: (Reflex t, MonadFix m, MonadHold t m)
=> V.Button
-> VtyWidget t m (Event t Drag)
drag btn = do
inp <- input
let f :: Drag -> V.Event -> Maybe Drag
f (Drag from _ _ mods end) = \case
V.EvMouseDown x y btn' mods'
| end -> Just $ Drag (x,y) (x,y) btn' mods' False
| btn == btn' -> Just $ Drag from (x,y) btn mods' False
| otherwise -> Nothing -- Ignore other buttons.
V.EvMouseUp x y (Just btn')
| end -> Nothing
| btn == btn' -> Just $ Drag from (x,y) btn mods True
| otherwise -> Nothing
V.EvMouseUp x y Nothing -- Terminal doesn't specify mouse up button,
-- assume it's the right one.
| end -> Nothing
| otherwise -> Just $ Drag from (x,y) btn mods True
_ -> Nothing
rec let newDrag = attachWithMaybe f (current dragD) inp
dragD <- holdDyn (Drag (0,0) (0,0) V.BLeft [] True) -- gross, but ok.
newDrag
return (updated dragD)
mouseDown
:: (Reflex t, Monad m)
=> V.Button
-> VtyWidget t m (Event t VtyEvent)
mouseDown btn = do
i <- input
return $ fforMaybe i $ \x -> case x of
V.EvMouseDown _ _ btn' _ -> if btn == btn' then Just x else Nothing
_ -> Nothing
-- | A plain split of the available space into vertically stacked panes.
-- No visual separator is built in here.
splitV :: (Reflex t, Monad m)
=> Dynamic t (Int -> Int)
-- ^ Function used to determine size of first pane based on available size
-> Dynamic t (Bool, Bool)
-- ^ How to focus the two sub-panes, given that we are focused.
-> VtyWidget t m a
-- ^ Widget for first pane
-> VtyWidget t m b
-- ^ Widget for second pane
-> VtyWidget t m (a,b)
splitV sizeFunD focD wA wB = do
sz <- displaySize
let regA = (\f (w,h) -> Region 0 0 w (f h)) <$> sizeFunD <*> sz
regB = (\(w,h) (Region _ _ _ hA) -> Region 0 hA w (h - hA)) <$> sz <*> regA
ra <- pane regA (fst <$> focD) wA
rb <- pane regB (snd <$> focD) wB
return (ra,rb)
-- | A split of the available space into two parts with a draggable separator.
-- Starts with half the space allocated to each, and the first pane has focus.
-- Clicking in a pane switches focus.
splitVDrag :: (Reflex t, MonadFix m, MonadHold t m)
=> VtyWidget t m a
-> VtyWidget t m b
-> VtyWidget t m (a,b)
splitVDrag wA wB = do
sz <- displaySize
(_, h0) <- sample $ current sz
dragE <- drag V.BLeft
let splitter0 = h0 `div` 2
rec splitterCheckpoint <- holdDyn splitter0 $ fst <$> ffilter snd dragSplitter
splitterPos <- holdDyn splitter0 $ fst <$> dragSplitter
let dragSplitter = fforMaybe (attach (current splitterCheckpoint) dragE) $ \(splitterY, Drag (_, fromY) (_, toY) _ _ end) ->
if splitterY == fromY then Just (toY, end) else Nothing
regA = (\(w,_) sp -> Region 0 0 w sp) <$> sz <*> splitterPos
regS = (\(w,_) sp -> Region 0 sp w 1) <$> sz <*> splitterPos
regB = (\(w,h) sp -> Region 0 (sp + 1) w (h - sp - 1)) <$> sz <*> splitterPos
focA <- holdDyn True $ leftmost
[ True <$ mA
, False <$ mB
]
(mA, rA) <- pane regA focA $ withMouseDown wA
tellImages $ ffor (current regS) $ \r -> [withinImage r (V.string mempty (replicate (_region_width r) '='))]
(mB, rB) <- pane regB (not <$> focA) $ withMouseDown wB
return (rA, rB)
where
withMouseDown x = do
m <- mouseDown V.BLeft
x' <- x
return (m, x')
fractionSz :: Double -> Int -> Int
fractionSz x h = round (fromIntegral h * x)
modifyImages
:: (Reflex t, MonadHold t m, MonadFix m)
=> Behavior t ([Image] -> [Image])
-> VtyWidget t m a
-> VtyWidget t m a
modifyImages f (VtyWidget w) = VtyWidget $ flip censor w $ \wo ->
wo { _vtyWidgetOut_images = f <*> (_vtyWidgetOut_images wo) }
box :: (Monad m, Reflex t)
=> VtyWidget t m a
-> VtyWidget t m a
box child = do
sz <- displaySize
let boxReg = ffor (current sz) $ \(w,h) -> Region 0 0 w h
innerReg = ffor sz $ \(w,h) -> Region 1 1 (w - 2) (h - 2)
tellImages (fmap boxImages boxReg)
tellImages (fmap (\r -> [regionBlankImage r]) (current innerReg))
pane innerReg (pure True) child
where
boxImages :: Region -> [Image]
boxImages r@(Region left top width height) =
let hBorder = V.string mempty $ replicate width '-'
vBorder = wrapString 1 mempty $ replicate (height - 2) '|'
in [ withinImage (r { _region_height = 1 }) hBorder
, withinImage (Region left (top + 1) 1 (height - 2)) vBorder
, withinImage (Region (left + width - 1) (top + 1) 1 (height - 2)) vBorder
, withinImage (r { _region_top = top + height - 1 }) hBorder
]
string :: (Reflex t, Monad m) => Behavior t String -> VtyWidget t m ()
string msg = do
dw <- displayWidth
let img = (\w s -> [wrapString w mempty s]) <$> current dw <*> msg
tellImages img
regionBlankImage :: Region -> Image
regionBlankImage r@(Region _ _ width height) =
withinImage r $ wrapString width V.defAttr $ replicate (width * height) ' '
withinImage :: Region -> Image -> Image
withinImage (Region left top width height)
| width < 0 || height < 0 = withinImage (Region left top 0 0)
| otherwise = V.translate left top . V.crop width height
wrapString :: Int -> Attr -> String -> Image
wrapString maxWidth attrs = V.vertCat . concatMap (fmap (V.string attrs) . fmap (take maxWidth) . takeWhile (not . null) . iterate (drop maxWidth)) . lines