@@ -5,15 +5,39 @@
5{-# LANGUAGE RankNTypes #-}
5{-# LANGUAGE RankNTypes #-}
6{-# LANGUAGE ScopedTypeVariables #-}
6{-# LANGUAGE ScopedTypeVariables #-}
7{-# LANGUAGE TypeFamilies #-}
7{-# LANGUAGE TypeFamilies #-}
8+{-# LANGUAGE FunctionalDependencies #-}
9+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
10+{-# LANGUAGE ConstraintKinds #-}
12+{-# OPTIONS_GHC -Wall #-}
9module Reflex.Vty
14module Reflex.Vty
11 , VtyResult(..)
16 , VtyResult(..)
12 , runVtyApp
18 , runVtyApp
13 , runVtyAppWith
19 , runVtyAppWith
14− , HasDisplayRegion(..)
20+ , HasDisplaySize(..)
24+ , regionToDisplayRegion
40+import Control.Applicative
17import Control.Concurrent (forkIO, killThread)
41import Control.Concurrent (forkIO, killThread)
18import Control.Concurrent.Chan (newChan, readChan, writeChan)
42import Control.Concurrent.Chan (newChan, readChan, writeChan)
19import Control.Monad (forM, forM_, forever)
43import Control.Monad (forM, forM_, forever)
@@ -23,6 +47,8 @@ import Control.Monad.Identity (Identity(..))
23import Control.Monad.Primitive (PrimMonad)
47import Control.Monad.Primitive (PrimMonad)
24import Control.Monad.Ref (MonadRef, Ref, readRef)
48import Control.Monad.Ref (MonadRef, Ref, readRef)
25import Control.Monad.Trans.Reader
49import Control.Monad.Trans.Reader
50+import Control.Monad.Trans.Writer
51+import Control.Monad.Trans
26import Data.Dependent.Sum (DSum ((:=>)))
52import Data.Dependent.Sum (DSum ((:=>)))
27import Data.IORef (IORef)
53import Data.IORef (IORef)
28import Data.IORef (readIORef)
54import Data.IORef (readIORef)
@@ -33,6 +59,9 @@ import Reflex.Host.Class
33import Reflex.NotReady.Class
59import Reflex.NotReady.Class
34import Reflex.Spider.Orphans ()
60import Reflex.Spider.Orphans ()
35import qualified Graphics.Vty as V
61import qualified Graphics.Vty as V
62+import Graphics.Vty (DisplayRegion, Image, Attr)
64+type VtyEvent = V.Event
37-- | The output of a 'VtyApp'.
66-- | The output of a 'VtyApp'.
38data VtyResult t = VtyResult
67data VtyResult t = VtyResult
@@ -43,14 +72,7 @@ data VtyResult t = VtyResult
43 -- ^ An event that requests application termination.
72 -- ^ An event that requests application termination.
46−class HasDisplayRegion m where
47− displayRegion :: m V.DisplayRegion
49−instance Monad m => HasDisplayRegion (ReaderT V.DisplayRegion m) where
52−-- | A functional reactive vty application.
53−type VtyApp t m =
75+type MonadVtyApp t m =
54 ( Reflex t
76 ( Reflex t
55 , MonadHold t m
77 , MonadHold t m
56 , MonadFix m
78 , MonadFix m
@@ -66,10 +88,13 @@ type VtyApp t m =
66 , PerformEvent t m
88 , PerformEvent t m
67 , MonadIO m
89 , MonadIO m
68 , MonadIO (Performable m)
90 , MonadIO (Performable m)
93+-- | A functional reactive vty application.
94+type VtyApp t m = MonadVtyApp t m
96+ -- ^ The initial display size (updates to this come as events)
72− => Event t (V.Event)
97+ -> Event t (V.Event)
73 -- ^ Vty input events.
98 -- ^ Vty input events.
74 -> m (VtyResult t)
99 -> m (VtyResult t)
75 -- ^ The output of the 'VtyApp'. The application runs in a context that,
100 -- ^ The output of the 'VtyApp'. The application runs in a context that,
@@ -127,8 +152,9 @@ runVtyAppWith vty vtyGuest =
127 -- which they will be read and
152 -- which they will be read and
128 -- processed.
153 -- processed.
130− flip runReaderT displayRegion0 $
155+ vtyGuest displayRegion0 vtyEvent
131− vtyGuest vtyEvent -- The guest app is provided an
156+ -- The guest app is provided the
157+ -- initial display region and an
132 -- 'Event' of vty inputs.
158 -- 'Event' of vty inputs.
134 -- Reads the current value of the 'Picture' behavior and updates the
160 -- Reads the current value of the 'Picture' behavior and updates the
@@ -220,3 +246,162 @@ runVtyApp app = do
220 cfg <- liftIO V.standardIOConfig
246 cfg <- liftIO V.standardIOConfig
221 vty <- liftIO $ V.mkVty $ cfg { V.mouseMode = Just True }
247 vty <- liftIO $ V.mkVty $ cfg { V.mouseMode = Just True }
222 runVtyAppWith vty app
248 runVtyAppWith vty app
254+ { _region_left :: Int
255+ , _region_top :: Int
256+ , _region_width :: Int
257+ , _region_height :: Int
259+ deriving (Show, Read, Eq, Ord)
261+regionToDisplayRegion :: Region -> DisplayRegion
262+regionToDisplayRegion (Region _ _ w h) = (w, h)
264+region :: Region -> (DisplayRegion -> Image) -> [Image]
265+region r@(Region _ _ width height) i =
266+ [ within r $ V.resize width height $ i (width, height)
267+ , within r $ wrapString width V.defAttr $ replicate (width * height) ' '
270+within :: Region -> Image -> Image
271+within (Region left top width height) =
272+ V.translate left top . V.crop width height
274+wrapString :: Int -> Attr -> String -> Image
275+wrapString maxWidth attrs = V.vertCat . concatMap (fmap (V.string attrs) . fmap (take maxWidth) . takeWhile (not . null) . iterate (drop maxWidth)) . lines
277+box :: Region -> (DisplayRegion -> Image) -> [Image]
278+box r@(Region left top width height) i =
279+ let hBorder = V.string mempty $ replicate width '-'
280+ vBorder = wrapString 1 mempty $ replicate (height - 2) '|'
282+ { _region_left = left + 1
283+ , _region_top = top + 1
284+ , _region_width = width - 2
285+ , _region_height = height - 2
287+ in [ within (r { _region_height = 1 }) hBorder
288+ , within (Region left (top + 1) 1 (height - 2)) vBorder
289+ , within (Region (left + width - 1) (top + 1) 1 (height - 2)) vBorder
290+ , within (r { _region_top = top + height - 1 }) hBorder
291+ ] ++ region internal i
293+class HasDisplaySize t m | m -> t where
294+ displaySize :: m (Dynamic t (Int, Int))
296+class HasVtyInput t m | m -> t where
297+ input :: m (Event t VtyEvent)
299+class HasFocus t m | m -> t where
300+ focus :: m (Dynamic t Bool)
302+data VtyWidgetCtx t = VtyWidgetCtx
303+ { _vtyWidgetCtx_size :: Dynamic t (Int,Int) -- ^ The width and height of the region allocated to the widget.
304+ , _vtyWidgetCtx_focus :: Dynamic t Bool -- ^ Whether the widget should behave as if it has focus for keyboard input.
305+ , _vtyWidgetCtx_input :: Event t VtyEvent -- ^ User input events that the widget's parent chooses to share. These will generally
306+ -- be filtered for relevance:
307+ -- * Keyboard inputs are restricted to focused widgets
308+ -- * Mouse inputs are restricted to the region in which the widget resides.
311+data VtyWidgetOut t = VtyWidgetOut
312+ { _vtyWidgetOut_images :: Behavior t [Image]
313+ , _vtyWidgetOut_shutdown :: Event t ()
316+instance (Reflex t) => Semigroup (VtyWidgetOut t) where
317+ wo <> wo' = VtyWidgetOut
318+ { _vtyWidgetOut_images = _vtyWidgetOut_images wo <> _vtyWidgetOut_images wo'
319+ , _vtyWidgetOut_shutdown = _vtyWidgetOut_shutdown wo <> _vtyWidgetOut_shutdown wo'
322+instance (Reflex t) => Monoid (VtyWidgetOut t) where
323+ mempty = VtyWidgetOut mempty mempty
324+ mappend wo wo' = wo <> wo'
326+newtype VtyWidget t m a = VtyWidget { unVtyWidget :: WriterT (VtyWidgetOut t) (ReaderT (VtyWidgetCtx t) m) a }
327+ deriving (Functor, Applicative, Monad, MonadSample t, MonadHold t)
329+runVtyWidget :: (MonadFix m, Reflex t)
332+ -> m (a, VtyWidgetOut t)
333+runVtyWidget ctx w = runReaderT (runWriterT (unVtyWidget w)) ctx
335+mainVtyWidget :: (forall t m. MonadVtyApp t m => VtyWidget t m ()) -> IO ()
336+mainVtyWidget child =
337+ runVtyApp $ \dr0 inp -> do
338+ size <- holdDyn dr0 $ fforMaybe inp $ \case
339+ V.EvResize w h -> Just (w, h)
341+ let ctx = VtyWidgetCtx
342+ { _vtyWidgetCtx_size = size
343+ , _vtyWidgetCtx_input = inp
344+ , _vtyWidgetCtx_focus = constDyn True
346+ ((), wo) <- runVtyWidget ctx child
348+ { _vtyResult_picture = fmap V.picForLayers (_vtyWidgetOut_images wo)
349+ , _vtyResult_shutdown = _vtyWidgetOut_shutdown wo
352+modifyImages :: (Reflex t, MonadHold t m, MonadFix m)
353+ => Behavior t ([Image] -> [Image]) -> VtyWidget t m a -> VtyWidget t m a
354+modifyImages f (VtyWidget w) = VtyWidget $
355+ censor (\wo -> wo { _vtyWidgetOut_images = f <*> (_vtyWidgetOut_images wo) })
358+instance (Reflex t, Monad m) => HasDisplaySize t (VtyWidget t m) where
359+ displaySize = VtyWidget . lift $ asks _vtyWidgetCtx_size
361+instance (Reflex t, Monad m) => HasVtyInput t (VtyWidget t m) where
362+ input = VtyWidget . lift $ asks _vtyWidgetCtx_input
364+instance (Reflex t, Monad m) => HasFocus t (VtyWidget t m) where
365+ focus = VtyWidget . lift $ asks _vtyWidgetCtx_focus
367+tellImages :: (Reflex t, Monad m) => Behavior t [Image] -> VtyWidget t m ()
368+tellImages imgs = VtyWidget $ tell (mempty { _vtyWidgetOut_images = imgs })
370+tellShutdown :: (Reflex t, Monad m) => Event t () -> VtyWidget t m ()
371+tellShutdown sd = VtyWidget $ tell (mempty { _vtyWidgetOut_shutdown = sd })
373+pane :: (MonadFix m, Reflex t)
374+ => Dynamic t Region -- ^ Region into which we should draw the widget (in coordinates relative to our own)
375+ -> Dynamic t Bool -- ^ Whether the widget is focused
378+pane reg foc w = VtyWidget $ do
380+ let ctx' = VtyWidgetCtx
381+ { _vtyWidgetCtx_input = fmapMaybe id $
382+ attachWith (\(r,f) e -> filterInput r f e)
383+ (liftA2 (,) (current reg) (current foc))
384+ (_vtyWidgetCtx_input ctx)
385+ , _vtyWidgetCtx_focus = foc
386+ , _vtyWidgetCtx_size = fmap regionToDisplayRegion reg }
387+ (result, wo) <- lift . lift $ runVtyWidget ctx' w
388+ let images = _vtyWidgetOut_images wo
389+ images' = liftA2 (\r is -> map (within r) is) (current reg) images
390+ wo' = wo { _vtyWidgetOut_images = images' }
394+filterInput :: Region -> Bool -> VtyEvent -> Maybe VtyEvent
395+filterInput r focused e = case e of
396+ V.EvKey _ _ | not focused -> Nothing
397+ V.EvMouseDown x y btn m -> mouse (\u v -> V.EvMouseDown u v btn m) x y
398+ V.EvMouseUp x y btn -> mouse (\u v -> V.EvMouseUp u v btn) x y
404+ , x >= _region_width r
405+ , y >= _region_height r ] = Nothing
407+ Just (con (x + _region_left r) (y + _region_top r))