on develop help sign in
Create Reflex.Vty.Widget.Layout; Make shutdown cleaner on exceptions
commit195220d21c762c05a480218647f872cb56f04fb4 authorAli Abrar <aliabrar@gmail.com> dateSat 3 Aug 2019, 21:14 (7 years ago) parent3412d17
9 files changed+352−263
reflex-vty.cabal+1 src-bin/example.hs+32−13 src/Control/Monad/NodeId.hs+16−1 src/Data/Text/Zipper.hs+4 src/Reflex/Vty.hs+2 src/Reflex/Vty/Host.hs+8−6 src/Reflex/Vty/Widget.hs+3−242 src/Reflex/Vty/Widget/Input/Text.hs+2−1 src/Reflex/Vty/Widget/Layout.hs+284
reflex-vty.cabal+1
@@ -23,6 +23,7 @@ library
2323 , Reflex.Vty.Widget
2424 , Reflex.Vty.Widget.Input
2525 , Reflex.Vty.Widget.Input.Text
26+ , Reflex.Vty.Widget.Layout
2627 , Data.Text.Zipper
2728 , Reflex.Class.Switchable
2829 , Reflex.Spider.Orphans
src-bin/example.hs+32−13
@@ -7,7 +7,6 @@
77{-# LANGUAGE RankNTypes #-}
88{-# LANGUAGE RecursiveDo #-}
99{-# LANGUAGE ScopedTypeVariables #-}
10−{-# LANGUAGE TupleSections #-}
1110{-# OPTIONS_GHC -threaded #-}
1211
1312import Control.Applicative
@@ -31,9 +30,6 @@ data Example = Example_TextEditor
3130 | Example_Todo
3231 deriving (Show, Read, Eq, Ord, Enum, Bounded)
3332
34−div' :: (Integral a, Applicative f) => f a -> f a -> f a
35−div' = liftA2 div
36−
3733main :: IO ()
3834main = mainWidget $ do
3935 inp <- input
@@ -68,13 +64,18 @@ taskList
6864taskList = do
6965 let btn = textButtonStatic def "Add another task"
7066 inp <- input
71− rec let todos' = todos [Todo "Find reflex-vty" True, Todo "Become functional reactive" False, Todo "Make vty apps" False] $ leftmost
67+ let todos0 =
68+ [ Todo "Find reflex-vty" True
69+ , Todo "Become functional reactive" False
70+ , Todo "Make vty apps" False
71+ ]
72+ rec let todos' = todos todos0 $ leftmost
7273 [ () <$ e
7374 , fforMaybe inp $ \case
7475 V.EvKey V.KEnter [] -> Just ()
7576 _ -> Nothing
7677 ]
77− (m, (e, _)) <- splitV (pure (subtract 6)) (pure (True, True)) todos' $ do
78+ (m, (e, _)) <- splitV (pure (subtract 6)) (pure (True, True)) todos' $
7879 splitV (pure (subtract 3)) (pure (True, True)) btn (display $ Map.size <$> current m)
7980 return ()
8081
@@ -88,8 +89,17 @@ testBoxes = do
8889 region2 = DynRegion (div' dw 4) (div' dh 4) (2 * div' dw 3) (2 * div' dh 3)
8990 pane region1 (constDyn False) . boxStatic singleBoxStyle $ debugInput
9091 _ <- pane region2 (constDyn True) . boxStatic singleBoxStyle $
91− splitVDrag (hRule doubleBoxStyle) (boxStatic roundedBoxStyle $ multilineTextInput def) (boxStatic roundedBoxStyle dragTest)
92+ let cfg = def
93+ { _textInputConfig_initialValue =
94+ "This box is a text input. The box below responds to mouse drag inputs. You can also drag the separator between the boxes to resize them."
95+ }
96+ textBox = boxStatic roundedBoxStyle $ multilineTextInput cfg
97+ dragBox = boxStatic roundedBoxStyle dragTest
98+ in splitVDrag (hRule doubleBoxStyle) textBox dragBox
9299 return ()
100+ where
101+ div' :: (Integral a, Applicative f) => f a -> f a -> f a
102+ div' = liftA2 div
93103
94104debugFocus :: (Reflex t, Monad m) => VtyWidget t m ()
95105debugFocus = do
@@ -148,7 +158,7 @@ todo t0 = do
148158 let deleteSelf = attachWithMaybe backspaceOnEmpty (current $ _textInput_value v) i
149159 return (v, deleteSelf)
150160 return $ TodoOutput
151− { _todoOutput_todo = Todo <$> (_textInput_value ti) <*> value
161+ { _todoOutput_todo = Todo <$> _textInput_value ti <*> value
152162 , _todoOutput_delete = d
153163 , _todoOutput_height = _textInput_lines ti
154164 }
@@ -158,7 +168,15 @@ todo t0 = do
158168 _ -> Nothing
159169
160170todos
161− :: forall t m. (MonadHold t m, MonadFix m, Reflex t, Adjustable t m, NotReady t m, PostBuild t m, MonadNodeId m)
171+ :: forall t m.
172+ ( MonadHold t m
173+ , MonadFix m
174+ , Reflex t
175+ , Adjustable t m
176+ , NotReady t m
177+ , PostBuild t m
178+ , MonadNodeId m
179+ )
162180 => [Todo]
163181 -> Event t ()
164182 -> VtyWidget t m (Dynamic t (Map Int (TodoOutput t)))
@@ -167,10 +185,11 @@ todos todos0 newTodo = do
167185 rec tabNav <- tabNavigation
168186 let insertNav = 1 <$ insert
169187 nav = leftmost [tabNav, insertNav]
188+ tileCfg = def { _tileConfig_constraint = pure $ Constraint_Fixed 1}
170189 listOut <- runLayout (pure Orientation_Column) 0 nav $
171− listHoldWithKey todosMap0 updates $ \k t -> tile (pure $ Constraint_Fixed 1) (pure True) $ do
190+ listHoldWithKey todosMap0 updates $ \k t -> tile tileCfg $ do
172191 let sel = select selectOnDelete $ Const2 k
173− click <- fmap (const ()) <$> mouseDown V.BLeft
192+ click <- void <$> mouseDown V.BLeft
174193 pb <- getPostBuild
175194 let focusMe = leftmost [ click, sel, pb ]
176195 r <- todo t
@@ -179,11 +198,11 @@ todos todos0 newTodo = do
179198 updates = leftmost [insert, delete]
180199 todoDelete = switch . current $
181200 leftmost . Map.elems . Map.mapWithKey (\k -> (k <$) . _todoOutput_delete) <$> listOut
182− todosMap = joinDynThroughMap $ fmap (_todoOutput_todo) <$> listOut
201+ todosMap = joinDynThroughMap $ fmap _todoOutput_todo <$> listOut
183202 insert = ffor (tag (current todosMap) newTodo) $ \m -> case Map.lookupMax m of
184203 Nothing -> Map.singleton 0 $ Just $ Todo "" False
185204 Just (k, _) -> Map.singleton (k+1) $ Just $ Todo "" False
186− selectOnDelete = fanMap $ fmap (flip Map.singleton ()) $ attachWithMaybe
205+ selectOnDelete = fanMap $ (`Map.singleton` ()) <$> attachWithMaybe
187206 (\m k -> let (before, after) = Map.split k m
188207 in fmap fst $ Map.lookupMax before <|> Map.lookupMin after)
189208 (current todosMap)
src/Control/Monad/NodeId.hs+16−1
@@ -1,10 +1,19 @@
1+{-|
2+Module: Control.Monad.NodeId
3+Description: Monad providing a supply of unique identifiers
4+-}
15{-# LANGUAGE DefaultSignatures #-}
26{-# LANGUAGE FlexibleInstances #-}
37{-# LANGUAGE GADTs #-}
48{-# LANGUAGE GeneralizedNewtypeDeriving #-}
59{-# LANGUAGE MultiParamTypeClasses #-}
610{-# LANGUAGE UndecidableInstances #-}
7−module Control.Monad.NodeId where
11+module Control.Monad.NodeId
12+ ( NodeId
13+ , MonadNodeId (..)
14+ , NodeIdT
15+ , runNodeIdT
16+ ) where
817
918import Control.Monad.Reader
1019import Control.Monad.Ref
@@ -13,14 +22,19 @@ import Data.IORef
1322import Reflex
1423import Reflex.Host.Class
1524
25+-- | A unique identifier with respect to the 'runNodeIdT' in which it was generated
1626newtype NodeId = NodeId Integer
1727 deriving (Eq, Ord, Show)
1828
29+-- | Members of this class can request new identifiers that are unique in the action
30+-- in which they are obtained (i.e., all calls to 'getNextNodeId' in a given 'runNodeIdT'
31+-- will produce unique results)
1932class Monad m => MonadNodeId m where
2033 getNextNodeId :: m NodeId
2134 default getNextNodeId :: (MonadTrans t, MonadNodeId n, m ~ t n) => m NodeId
2235 getNextNodeId = lift getNextNodeId
2336
37+-- | A monad transformer that internally keeps track of the next 'NodeId'
2438newtype NodeIdT m a = NodeIdT { unNodeIdT :: ReaderT (IORef NodeId) m a }
2539 deriving
2640 ( Functor
@@ -52,6 +66,7 @@ instance Adjustable t m => Adjustable t (NodeIdT m) where
5266 traverseDMapWithKeyWithAdjust f m e = NodeIdT $ traverseDMapWithKeyWithAdjust (\k v -> unNodeIdT $ f k v) m e
5367 traverseDMapWithKeyWithAdjustWithMove f m e = NodeIdT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unNodeIdT $ f k v) m e
5468
69+-- | Runs a 'NodeIdT' action
5570runNodeIdT :: MonadIO m => NodeIdT m a -> m a
5671runNodeIdT a = do
5772 ref <- liftIO $ newIORef $ NodeId 0
src/Data/Text/Zipper.hs+4
@@ -16,6 +16,7 @@ import Data.Char (isSpace)
1616import Data.Map (Map)
1717import qualified Data.Map as Map
1818import Data.Maybe (fromMaybe)
19+import Data.String
1920import Control.Monad.State (evalState, forM, get, put)
2021
2122import Data.Text (Text)
@@ -35,6 +36,9 @@ data TextZipper = TextZipper
3536 }
3637 deriving (Show)
3738
39+instance IsString TextZipper where
40+ fromString = fromText . T.pack
41+
3842-- | Move the cursor left one character, if possible
3943left :: TextZipper -> TextZipper
4044left = leftN 1
src/Reflex/Vty.hs+2
@@ -15,6 +15,7 @@ module Reflex.Vty
1515 , module Reflex.Vty.Host
1616 , module Reflex.Vty.Widget
1717 , module Reflex.Vty.Widget.Input
18+ , module Reflex.Vty.Widget.Layout
1819 , module Control.Monad.NodeId
1920 ) where
2021
@@ -22,5 +23,6 @@ import Reflex
2223import Reflex.Vty.Host
2324import Reflex.Vty.Widget
2425import Reflex.Vty.Widget.Input
26+import Reflex.Vty.Widget.Layout
2527
2628import Control.Monad.NodeId
src/Reflex/Vty/Host.hs+8−6
@@ -20,6 +20,7 @@ module Reflex.Vty.Host
2020
2121import Control.Concurrent (forkIO, killThread)
2222import Control.Concurrent.Chan (newChan, readChan, writeChan)
23+import Control.Exception (onException)
2324import Control.Monad (forM, forM_, forever)
2425import Control.Monad.Fix (MonadFix, fix)
2526import Control.Monad.IO.Class (liftIO, MonadIO)
@@ -44,7 +45,7 @@ type VtyEvent = V.Event
4445-- | The output of a 'VtyApp'.
4546data VtyResult t = VtyResult
4647 { _vtyResult_picture :: Behavior t V.Picture
47− -- ^ The current vty output. 'runVtyAppWith' samples this value every time an
48+ -- ^ The current vty output. 'runVtyAppWithHandle' samples this value every time an
4849 -- event fires and updates the display.
4950 , _vtyResult_shutdown :: Event t ()
5051 -- ^ An event that requests application termination.
@@ -84,14 +85,15 @@ type VtyApp t m = MonadVtyApp t m
8485 -- app instantiation ('PostBuild'), and allows actions to be run upon
8586 -- occurrences of events ('PerformEvent').
8687
87−-- | Runs a 'VtyApp' in a given 'Vty'.
88+-- | Runs a 'VtyApp' in a given 'Graphics.Vty.Vty'.
8889runVtyAppWithHandle
8990 :: V.Vty
90− -- ^ A 'Vty' handle.
91+ -- ^ A 'Graphics.Vty.Vty' handle.
9192 -> (forall t m. VtyApp t m)
9293 -- ^ A functional reactive vty application.
9394 -> IO ()
94−runVtyAppWithHandle vty vtyGuest =
95+runVtyAppWithHandle vty vtyGuest = flip onException (V.shutdown vty) $
96+
9597 -- We are using the 'Spider' implementation of reflex. Running the host
9698 -- allows us to take actions on the FRP timeline. The scoped type signature
9799 -- specifies that our host runs on the Global timeline.
@@ -195,7 +197,7 @@ runVtyAppWithHandle vty vtyGuest =
195197 if or stop
196198 then liftIO $ do -- If we received a shutdown 'Event'
197199 killThread nextEventThread -- then stop reading input events and
198− V.shutdown vty -- call the 'Vty's shutdown command.
200+ V.shutdown vty -- call the 'Graphics.Vty.Vty's shutdown command.
199201
200202 else do -- Otherwise, update the display and loop.
201203 updateVty
@@ -219,7 +221,7 @@ runVtyAppWithHandle vty vtyGuest =
219221 liftIO $ forM_ ers $ \(_ :=> TriggerInvocation _ cb) -> cb
220222 return a
221223
222−-- | Run a 'VtyApp' with a 'Vty' handle with a standard configuration.
224+-- | Run a 'VtyApp' with a 'Graphics.Vty.Vty' handle with a standard configuration.
223225runVtyApp
224226 :: (forall t m. VtyApp t m)
225227 -> IO ()
src/Reflex/Vty/Widget.hs+3−242
@@ -9,7 +9,6 @@ Description: Basic set of widgets and building blocks for reflex-vty application
99{-# LANGUAGE FunctionalDependencies #-}
1010{-# LANGUAGE GeneralizedNewtypeDeriving #-}
1111{-# LANGUAGE LambdaCase #-}
12−{-# LANGUAGE MultiParamTypeClasses #-}
1312{-# LANGUAGE RankNTypes #-}
1413{-# LANGUAGE RecursiveDo #-}
1514{-# LANGUAGE ScopedTypeVariables #-}
@@ -62,33 +61,13 @@ module Reflex.Vty.Widget
6261 , keys
6362 , keyCombos
6463 , blank
65− , Orientation(..)
66− , Constraint(..)
67− , Layout
68− , runLayout
69− , tile
70− , fixed
71− , stretch
72− , col
73− , row
74− , tabNavigation
75− , askOrientation
7664 ) where
7765
7866import Control.Applicative (liftA2)
7967import Control.Monad.Fix (MonadFix)
80−import Control.Monad.Trans.Class
81−import Control.Monad.Trans (lift)
8268import Control.Monad.Reader
83−import Data.Bimap (Bimap)
84−import qualified Data.Bimap as Bimap
69+import Control.Monad.Trans (lift)
8570import Data.Default (Default(..))
86−import Data.Map (Map)
87−import qualified Data.Map as Map
88−import Data.Maybe (fromMaybe)
89−import Data.Monoid hiding (First(..))
90−import Data.Ratio
91−import Data.Semigroup (First(..))
9271import Data.Set (Set)
9372import qualified Data.Set as Set
9473import Data.Text (Text)
@@ -97,8 +76,8 @@ import qualified Data.Text.Zipper as TZ
9776import Graphics.Vty (Image)
9877import qualified Graphics.Vty as V
9978import Reflex
100−import Reflex.Host.Class (MonadReflexCreateTrigger)
10179import Reflex.Class ()
80+import Reflex.Host.Class (MonadReflexCreateTrigger)
10281
10382import Reflex.Vty.Host
10483
@@ -646,224 +625,6 @@ display
646625 -> VtyWidget t m ()
647626display a = text $ T.pack . show <$> a
648627
649−data Constraint = Constraint_Fixed Int
650− | Constraint_Min Int
651− deriving (Show, Read, Eq, Ord)
652−
653−-- | Compute the size of each widget "@k@" based on the total set of 'Constraint's
654−computeSizes
655− :: Ord k
656− => Int
657− -> Map k (a, Constraint)
658− -> Map k (a, Int)
659−computeSizes available constraints =
660− let minTotal = sum $ ffor (Map.elems constraints) $ \case
661− (_, Constraint_Fixed n) -> n
662− (_, Constraint_Min n) -> n
663− leftover = max 0 (available - minTotal)
664− numStretch = Map.size $ Map.filter (isMin . snd) constraints
665− szStretch = floor $ leftover % (max numStretch 1)
666− adjustment = max 0 $ available - minTotal - szStretch * numStretch
667− in snd $ Map.mapAccum (\adj (a, c) -> case c of
668− Constraint_Fixed n -> (adj, (a, n))
669− Constraint_Min n -> (0, (a, n + szStretch + adj))) adjustment constraints
670− where
671− isMin (Constraint_Min _) = True
672− isMin _ = False
673−
674−computeEdges :: (Ord k) => Map k (a, Int) -> Map k (a, (Int, Int))
675−computeEdges = fst . Map.foldlWithKey' (\(m, offset) k (a, sz) ->
676− (Map.insert k (a, (offset, sz)) m, sz + offset)) (Map.empty, 0)
677−
628+-- | A widget that draws nothing
678629blank :: Monad m => VtyWidget t m ()
679630blank = return ()
680−
681−-- | The main-axis orientation of a 'StackWidget' widget
682−data Orientation = Orientation_Column
683− | Orientation_Row
684− deriving (Show, Read, Eq, Ord)
685−
686−data LayoutSegment = LayoutSegment
687− { _layoutSegment_offset :: Int
688− , _layoutSegment_size :: Int
689− }
690−
691−data LayoutCtx t = LayoutCtx
692− { _layoutCtx_regions :: Dynamic t (Map NodeId LayoutSegment)
693− , _layoutCtx_focusDemux :: Demux t (Maybe NodeId)
694− , _layoutCtx_orientation :: Dynamic t Orientation
695− }
696−
697−newtype Layout t m a = Layout
698− { unLayout :: EventWriterT t (First NodeId)
699− (DynamicWriterT t (Endo [(NodeId, (Bool, Constraint))])
700− (ReaderT (LayoutCtx t)
701− (VtyWidget t m))) a
702− } deriving
703− ( Functor
704− , Applicative
705− , Monad
706− , MonadHold t
707− , MonadSample t
708− , MonadFix
709− , TriggerEvent t
710− , PerformEvent t
711− , NotReady t
712− , MonadReflexCreateTrigger t
713− , HasDisplaySize t
714− , MonadNodeId
715− )
716−
717−instance MonadTrans (Layout t) where
718− lift x = Layout $ lift $ lift $ lift $ lift x
719−
720−instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (Layout t m) where
721− runWithReplace (Layout a) e = Layout $ runWithReplace a $ fmap unLayout e
722− traverseIntMapWithKeyWithAdjust f m e = Layout $ traverseIntMapWithKeyWithAdjust (\k v -> unLayout $ f k v) m e
723− traverseDMapWithKeyWithAdjust f m e = Layout $ traverseDMapWithKeyWithAdjust (\k v -> unLayout $ f k v) m e
724− traverseDMapWithKeyWithAdjustWithMove f m e = Layout $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unLayout $ f k v) m e
725−
726−runLayout
727− :: (MonadFix m, MonadHold t m, PostBuild t m, Monad m, MonadNodeId m)
728− => Dynamic t Orientation
729− -> Int
730− -> Event t Int
731− -> Layout t m a
732− -> VtyWidget t m a
733−runLayout ddir focus0 focusShift (Layout child) = do
734− dw <- displayWidth
735− dh <- displayHeight
736− let main = ffor3 ddir dw dh $ \d w h -> case d of
737− Orientation_Column -> h
738− Orientation_Row -> w
739− pb <- getPostBuild
740− rec ((a, focusReq), queriesEndo) <- runReaderT (runDynamicWriterT $ runEventWriterT child) $ LayoutCtx solutionMap focusDemux ddir
741− let queries = flip appEndo [] <$> queriesEndo
742− solution = ffor2 main queries $ \sz qs -> Map.fromList
743− . Map.elems
744− . computeEdges
745− . computeSizes sz
746− . fmap (fmap snd)
747− . Map.fromList
748− . zip [0::Integer ..]
749− $ qs
750− solutionMap = ffor solution $ \ss -> ffor ss $ \(offset, sz) -> LayoutSegment
751− { _layoutSegment_offset = offset
752− , _layoutSegment_size = sz
753− }
754− focusable = fmap (Bimap.fromList . zip [0..]) $
755− ffor queries $ \qs -> fforMaybe qs $ \(nodeId, (f, _)) ->
756− if f then Just nodeId else Nothing
757− adjustFocus
758− :: (Bimap Int NodeId, (Int, Maybe NodeId))
759− -> Either Int NodeId
760− -> (Int, Maybe NodeId)
761− adjustFocus (fm, (cur, _)) (Left shift) =
762− let ix = (cur + shift) `mod` (max 1 $ Bimap.size fm)
763− in (ix, Bimap.lookup ix fm)
764− adjustFocus (fm, (cur, _)) (Right goto) =
765− let ix = fromMaybe cur $ Bimap.lookupR goto fm
766− in (ix, Just goto)
767− focusChange = attachWith
768− adjustFocus
769− (current $ (,) <$> focusable <*> focussed)
770− $ leftmost [Left <$> focusShift, Left 0 <$ pb, Right . getFirst <$> focusReq]
771− -- A pair (Int, Maybe NodeId) which represents the index
772− -- that we're trying to focus, and the node that actually gets
773− -- focused (at that index) if it exists
774− focussed <- holdDyn (focus0, Nothing) focusChange
775− let focusDemux = demux $ snd <$> focussed
776− return a
777−
778−tile
779− :: (Reflex t, Monad m, MonadNodeId m)
780− => Dynamic t Constraint
781− -> Dynamic t Bool
782− -> VtyWidget t m (Event t x, a)
783− -> Layout t m a
784−tile con focusable child = do
785− nodeId <- getNextNodeId
786− Layout $ tellDyn $ ffor2 con focusable $ \c f -> Endo ((nodeId, (f, c)):)
787− seg <- Layout $ asks $
788− fmap (Map.findWithDefault (LayoutSegment 0 0) nodeId) . _layoutCtx_regions
789− dw <- displayWidth
790− dh <- displayHeight
791− o <- askOrientation
792− let cross = join $ ffor o $ \case
793− Orientation_Column -> dw
794− Orientation_Row -> dh
795− let reg = DynRegion
796− { _dynRegion_top = ffor2 seg o $ \s -> \case
797− Orientation_Column -> _layoutSegment_offset s
798− Orientation_Row -> 0
799− , _dynRegion_left = ffor2 seg o $ \s -> \case
800− Orientation_Column -> 0
801− Orientation_Row -> _layoutSegment_offset s
802− , _dynRegion_width = ffor3 seg cross o $ \s c -> \case
803− Orientation_Column -> c
804− Orientation_Row -> _layoutSegment_size s
805− , _dynRegion_height = ffor3 seg cross o $ \s c -> \case
806− Orientation_Column -> _layoutSegment_size s
807− Orientation_Row -> c
808− }
809− focussed <- Layout $ asks _layoutCtx_focusDemux
810− (focusReq, a) <- Layout $ lift $ lift $ lift $
811− pane reg (demuxed focussed $ Just nodeId) $ child
812− Layout $ tellEvent $ First nodeId <$ focusReq
813− return a
814−
815−data TileConfig t = TileConfig
816− { _tileConfig_constraint :: Dynamic t Constraint
817− , _tileConfig_focusable :: Dynamic t Bool
818− , _tileConfig_clickable :: Behavior t Bool
819− }
820−
821−instance Reflex t => Default (TileConfig t) where
822− def = TileConfig (pure $ Constraint_Min 0) (pure True) (pure True)
823−
824−fixed
825− :: (Reflex t, Monad m, MonadNodeId m)
826− => Dynamic t Int
827− -> VtyWidget t m a
828− -> Layout t m a
829−fixed sz = tile (Constraint_Fixed <$> sz) (pure True) . clickable
830−
831−stretch
832− :: (Reflex t, Monad m, MonadNodeId m)
833− => VtyWidget t m a
834− -> Layout t m a
835−stretch = tile (Constraint_Min <$> 0) (pure True) . clickable
836−
837−col
838− :: (MonadFix m, MonadHold t m, PostBuild t m, MonadNodeId m)
839− => Layout t m a
840− -> VtyWidget t m a
841−col child = do
842− nav <- tabNavigation
843− runLayout (pure Orientation_Column) 0 nav child
844−
845−row
846− :: (MonadFix m, MonadHold t m, PostBuild t m, MonadNodeId m)
847− => Layout t m a
848− -> VtyWidget t m a
849−row child = do
850− nav <- tabNavigation
851− runLayout (pure Orientation_Column) 0 nav child
852−
853−tabNavigation :: (Reflex t, Monad m) => VtyWidget t m (Event t Int)
854−tabNavigation = do
855− fwd <- fmap (const 1) <$> key (V.KChar '\t')
856− back <- fmap (const (-1)) <$> key V.KBackTab
857− return $ leftmost [fwd, back]
858−
859−clickable
860− :: (Reflex t, Monad m)
861− => VtyWidget t m a
862− -> VtyWidget t m (Event t (), a)
863−clickable child = do
864− click <- mouseDown V.BLeft
865− a <- child
866− return (() <$ click, a)
867−
868−askOrientation :: Monad m => Layout t m (Dynamic t Orientation)
869−askOrientation = Layout $ asks _layoutCtx_orientation
src/Reflex/Vty/Widget/Input/Text.hs+2−1
@@ -22,6 +22,7 @@ import qualified Graphics.Vty as V
2222import Reflex
2323
2424import Reflex.Vty.Widget
25+import Reflex.Vty.Widget.Layout
2526
2627-- | Configuration options for a 'textInput'. For more information on
2728-- 'TextZipper', see 'Data.Text.Zipper'.
@@ -121,7 +122,7 @@ images = map (V.horizCat . map spanToImage)
121122image :: [[Span V.Attr]] -> V.Image
122123image = V.vertCat . images
123124
124−-- | Turn a 'Span' into an 'Image'
125+-- | Turn a 'Span' into an 'Graphics.Vty.Image'
125126spanToImage :: Span V.Attr -> V.Image
126127spanToImage (Span attrs t) = V.text' attrs t
127128
src/Reflex/Vty/Widget/Layout.hsnew file+284
@@ -0,0 +1,284 @@
1+{-|
2+Module: Reflex.Vty.Widget.Layout
3+Description: Monad transformer and tools for arranging widgets and building screen layouts
4+-}
5+{-# LANGUAGE FlexibleInstances #-}
6+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
7+{-# LANGUAGE LambdaCase #-}
8+{-# LANGUAGE MultiParamTypeClasses #-}
9+{-# LANGUAGE RecursiveDo #-}
10+{-# LANGUAGE UndecidableInstances #-}
11+
12+module Reflex.Vty.Widget.Layout
13+ ( Orientation(..)
14+ , Constraint(..)
15+ , Layout
16+ , runLayout
17+ , TileConfig(..)
18+ , tile
19+ , fixed
20+ , stretch
21+ , col
22+ , row
23+ , tabNavigation
24+ , askOrientation
25+ ) where
26+
27+import Control.Monad.NodeId (NodeId, MonadNodeId(..))
28+import Control.Monad.Reader
29+import Data.Bimap (Bimap)
30+import qualified Data.Bimap as Bimap
31+import Data.Default (Default(..))
32+import Data.Map (Map)
33+import qualified Data.Map as Map
34+import Data.Maybe (fromMaybe)
35+import Data.Monoid hiding (First(..))
36+import Data.Ratio ((%))
37+import Data.Semigroup (First(..))
38+import qualified Graphics.Vty as V
39+
40+import Reflex
41+import Reflex.Host.Class (MonadReflexCreateTrigger)
42+import Reflex.Vty.Widget
43+
44+-- | The main-axis orientation of a 'Layout' widget
45+data Orientation = Orientation_Column
46+ | Orientation_Row
47+ deriving (Show, Read, Eq, Ord)
48+
49+data LayoutSegment = LayoutSegment
50+ { _layoutSegment_offset :: Int
51+ , _layoutSegment_size :: Int
52+ }
53+
54+data LayoutCtx t = LayoutCtx
55+ { _layoutCtx_regions :: Dynamic t (Map NodeId LayoutSegment)
56+ , _layoutCtx_focusDemux :: Demux t (Maybe NodeId)
57+ , _layoutCtx_orientation :: Dynamic t Orientation
58+ }
59+
60+-- | The Layout monad transformer keeps track of the configuration (e.g., 'Orientation') and
61+-- 'Constraint's of its child widgets, apportions vty real estate to each, and acts as a
62+-- switchboard for focus requests. See 'tile' and 'runLayout'.
63+newtype Layout t m a = Layout
64+ { unLayout :: EventWriterT t (First NodeId)
65+ (DynamicWriterT t (Endo [(NodeId, (Bool, Constraint))])
66+ (ReaderT (LayoutCtx t)
67+ (VtyWidget t m))) a
68+ } deriving
69+ ( Functor
70+ , Applicative
71+ , Monad
72+ , MonadHold t
73+ , MonadSample t
74+ , MonadFix
75+ , TriggerEvent t
76+ , PerformEvent t
77+ , NotReady t
78+ , MonadReflexCreateTrigger t
79+ , HasDisplaySize t
80+ , MonadNodeId
81+ )
82+
83+instance MonadTrans (Layout t) where
84+ lift x = Layout $ lift $ lift $ lift $ lift x
85+
86+instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (Layout t m) where
87+ runWithReplace (Layout a) e = Layout $ runWithReplace a $ fmap unLayout e
88+ traverseIntMapWithKeyWithAdjust f m e = Layout $ traverseIntMapWithKeyWithAdjust (\k v -> unLayout $ f k v) m e
89+ traverseDMapWithKeyWithAdjust f m e = Layout $ traverseDMapWithKeyWithAdjust (\k v -> unLayout $ f k v) m e
90+ traverseDMapWithKeyWithAdjustWithMove f m e = Layout $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unLayout $ f k v) m e
91+
92+-- | Run a 'Layout' action
93+runLayout
94+ :: (MonadFix m, MonadHold t m, PostBuild t m, Monad m, MonadNodeId m)
95+ => Dynamic t Orientation -- ^ The main-axis 'Orientation' of this 'Layout'
96+ -> Int -- ^ The positional index of the initially focused tile
97+ -> Event t Int -- ^ An event that shifts focus by a given number of tiles
98+ -> Layout t m a -- ^ The 'Layout' widget
99+ -> VtyWidget t m a
100+runLayout ddir focus0 focusShift (Layout child) = do
101+ dw <- displayWidth
102+ dh <- displayHeight
103+ let main = ffor3 ddir dw dh $ \d w h -> case d of
104+ Orientation_Column -> h
105+ Orientation_Row -> w
106+ pb <- getPostBuild
107+ rec ((a, focusReq), queriesEndo) <- runReaderT (runDynamicWriterT $ runEventWriterT child) $ LayoutCtx solutionMap focusDemux ddir
108+ let queries = flip appEndo [] <$> queriesEndo
109+ solution = ffor2 main queries $ \sz qs -> Map.fromList
110+ . Map.elems
111+ . computeEdges
112+ . computeSizes sz
113+ . fmap (fmap snd)
114+ . Map.fromList
115+ . zip [0::Integer ..]
116+ $ qs
117+ solutionMap = ffor solution $ \ss -> ffor ss $ \(offset, sz) -> LayoutSegment
118+ { _layoutSegment_offset = offset
119+ , _layoutSegment_size = sz
120+ }
121+ focusable = fmap (Bimap.fromList . zip [0..]) $
122+ ffor queries $ \qs -> fforMaybe qs $ \(nodeId, (f, _)) ->
123+ if f then Just nodeId else Nothing
124+ adjustFocus
125+ :: (Bimap Int NodeId, (Int, Maybe NodeId))
126+ -> Either Int NodeId
127+ -> (Int, Maybe NodeId)
128+ adjustFocus (fm, (cur, _)) (Left shift) =
129+ let ix = (cur + shift) `mod` (max 1 $ Bimap.size fm)
130+ in (ix, Bimap.lookup ix fm)
131+ adjustFocus (fm, (cur, _)) (Right goto) =
132+ let ix = fromMaybe cur $ Bimap.lookupR goto fm
133+ in (ix, Just goto)
134+ focusChange = attachWith
135+ adjustFocus
136+ (current $ (,) <$> focusable <*> focussed)
137+ $ leftmost [Left <$> focusShift, Left 0 <$ pb, Right . getFirst <$> focusReq]
138+ -- A pair (Int, Maybe NodeId) which represents the index
139+ -- that we're trying to focus, and the node that actually gets
140+ -- focused (at that index) if it exists
141+ focussed <- holdDyn (focus0, Nothing) focusChange
142+ let focusDemux = demux $ snd <$> focussed
143+ return a
144+
145+-- | Tiles are the basic building blocks of 'Layout' widgets. Each tile has a constraint
146+-- on its size and ability to grow and on whether it can be focused. It also allows its child
147+-- widget to request focus.
148+tile
149+ :: (Reflex t, Monad m, MonadNodeId m)
150+ => TileConfig t -- ^ The tile's configuration
151+ -> VtyWidget t m (Event t x, a) -- ^ A child widget. The 'Event' that it returns is used to request that it be focused.
152+ -> Layout t m a
153+tile (TileConfig con focusable) child = do
154+ nodeId <- getNextNodeId
155+ Layout $ tellDyn $ ffor2 con focusable $ \c f -> Endo ((nodeId, (f, c)):)
156+ seg <- Layout $ asks $
157+ fmap (Map.findWithDefault (LayoutSegment 0 0) nodeId) . _layoutCtx_regions
158+ dw <- displayWidth
159+ dh <- displayHeight
160+ o <- askOrientation
161+ let cross = join $ ffor o $ \case
162+ Orientation_Column -> dw
163+ Orientation_Row -> dh
164+ let reg = DynRegion
165+ { _dynRegion_top = ffor2 seg o $ \s -> \case
166+ Orientation_Column -> _layoutSegment_offset s
167+ Orientation_Row -> 0
168+ , _dynRegion_left = ffor2 seg o $ \s -> \case
169+ Orientation_Column -> 0
170+ Orientation_Row -> _layoutSegment_offset s
171+ , _dynRegion_width = ffor3 seg cross o $ \s c -> \case
172+ Orientation_Column -> c
173+ Orientation_Row -> _layoutSegment_size s
174+ , _dynRegion_height = ffor3 seg cross o $ \s c -> \case
175+ Orientation_Column -> _layoutSegment_size s
176+ Orientation_Row -> c
177+ }
178+ focussed <- Layout $ asks _layoutCtx_focusDemux
179+ (focusReq, a) <- Layout $ lift $ lift $ lift $
180+ pane reg (demuxed focussed $ Just nodeId) $ child
181+ Layout $ tellEvent $ First nodeId <$ focusReq
182+ return a
183+
184+-- | Configuration options for and constraints on 'tile'
185+data TileConfig t = TileConfig
186+ { _tileConfig_constraint :: Dynamic t Constraint
187+ -- ^ 'Constraint' on the tile's size
188+ , _tileConfig_focusable :: Dynamic t Bool
189+ -- ^ Whether the tile is focusable data TileConfig t = TileConfig
190+ }
191+
192+instance Reflex t => Default (TileConfig t) where
193+ def = TileConfig (pure $ Constraint_Min 0) (pure True)
194+
195+-- | A 'tile' of a fixed size that is focusable and gains focus on click
196+fixed
197+ :: (Reflex t, Monad m, MonadNodeId m)
198+ => Dynamic t Int
199+ -> VtyWidget t m a
200+ -> Layout t m a
201+fixed sz = tile (def { _tileConfig_constraint = Constraint_Fixed <$> sz }) . clickable
202+
203+-- | A 'tile' that can stretch (i.e., has no fixed size) and has a minimum size of 0.
204+-- This tile is focusable and gains focus on click.
205+stretch
206+ :: (Reflex t, Monad m, MonadNodeId m)
207+ => VtyWidget t m a
208+ -> Layout t m a
209+stretch = tile def . clickable
210+
211+-- | A version of 'runLayout' that arranges tiles in a column and uses 'tabNavigation' to
212+-- change tile focus.
213+col
214+ :: (MonadFix m, MonadHold t m, PostBuild t m, MonadNodeId m)
215+ => Layout t m a
216+ -> VtyWidget t m a
217+col child = do
218+ nav <- tabNavigation
219+ runLayout (pure Orientation_Column) 0 nav child
220+
221+-- | A version of 'runLayout' that arranges tiles in a row and uses 'tabNavigation' to
222+-- change tile focus.
223+row
224+ :: (MonadFix m, MonadHold t m, PostBuild t m, MonadNodeId m)
225+ => Layout t m a
226+ -> VtyWidget t m a
227+row child = do
228+ nav <- tabNavigation
229+ runLayout (pure Orientation_Column) 0 nav child
230+
231+-- | Produces an 'Event' that navigates forward one tile when the Tab key is pressed
232+-- and backward one tile when Shift+Tab is pressed.
233+tabNavigation :: (Reflex t, Monad m) => VtyWidget t m (Event t Int)
234+tabNavigation = do
235+ fwd <- fmap (const 1) <$> key (V.KChar '\t')
236+ back <- fmap (const (-1)) <$> key V.KBackTab
237+ return $ leftmost [fwd, back]
238+
239+-- | Captures the click event in a 'VtyWidget' context and returns it. Useful for
240+-- requesting focus when using 'tile'.
241+clickable
242+ :: (Reflex t, Monad m)
243+ => VtyWidget t m a
244+ -> VtyWidget t m (Event t (), a)
245+clickable child = do
246+ click <- mouseDown V.BLeft
247+ a <- child
248+ return (() <$ click, a)
249+
250+-- | Retrieve the current orientation of a 'Layout'
251+askOrientation :: Monad m => Layout t m (Dynamic t Orientation)
252+askOrientation = Layout $ asks _layoutCtx_orientation
253+
254+-- | Datatype representing constraints on a widget's size along the main axis (see 'Orientation')
255+data Constraint = Constraint_Fixed Int
256+ | Constraint_Min Int
257+ deriving (Show, Read, Eq, Ord)
258+
259+-- | Compute the size of each widget "@k@" based on the total set of 'Constraint's
260+computeSizes
261+ :: Ord k
262+ => Int
263+ -> Map k (a, Constraint)
264+ -> Map k (a, Int)
265+computeSizes available constraints =
266+ let minTotal = sum $ ffor (Map.elems constraints) $ \case
267+ (_, Constraint_Fixed n) -> n
268+ (_, Constraint_Min n) -> n
269+ leftover = max 0 (available - minTotal)
270+ numStretch = Map.size $ Map.filter (isMin . snd) constraints
271+ szStretch = floor $ leftover % (max numStretch 1)
272+ adjustment = max 0 $ available - minTotal - szStretch * numStretch
273+ in snd $ Map.mapAccum (\adj (a, c) -> case c of
274+ Constraint_Fixed n -> (adj, (a, n))
275+ Constraint_Min n -> (0, (a, n + szStretch + adj))) adjustment constraints
276+ where
277+ isMin (Constraint_Min _) = True
278+ isMin _ = False
279+
280+computeEdges :: (Ord k) => Map k (a, Int) -> Map k (a, (Int, Int))
281+computeEdges = fst . Map.foldlWithKey' (\(m, offset) k (a, sz) ->
282+ (Map.insert k (a, (offset, sz)) m, sz + offset)) (Map.empty, 0)
283+
284+