on develop help sign in
Get rid of VtyWidget and VtyWidgetCtx. Move functions to more appropriate modules; Consistent naming
commite3df5c0f63be9f5320023a55b8673cb1788f0265 authorAli Abrar <aliabrar@gmail.com> dateFri 26 Mar 2021, 01:44 (6 years ago) parentd8453a9
11 files changed+801−700
ChangeLog.md+75−57 reflex-vty.cabal+4 src/Reflex/Vty.hs+6 src/Reflex/Vty/Widget.hs+258−603 src/Reflex/Vty/Widget/Box.hs+122 src/Reflex/Vty/Widget/Input.hs+15−6 src/Reflex/Vty/Widget/Input/Mouse.hs+106 src/Reflex/Vty/Widget/Input/Text.hs+4−3 src/Reflex/Vty/Widget/Layout.hs+19−31 src/Reflex/Vty/Widget/Split.hs+93 src/Reflex/Vty/Widget/Text.hs+99
ChangeLog.md+75−57
@@ -1,63 +1,81 @@
11# Revision history for reflex-vty
22
33## 0.2.0.0
4−* Remove text-icu dependency and switch to `wcwidth` from vty package to compute character width in `Data.Text.Zipper`.
5−* `goToDisplayLinePosition` in `Data.Text.Zipper` correctly accounts for character width now.
6−* [#37](https://github.com/reflex-frp/reflex-vty/issues/37) `Layout` should support focus changes through nested layouts (thanks @pdlla for getting this started).
7−* _Breaking Change_: Layout and focus have been substantially refactored:
8− * Added `MonadFocus` to produce focusable elements, and manage focus state. See the "Focus" section of the Reflex.Vty.Widget.Layout module documentation.
9− * `Layout` no longer has any focus-tracking responsibility. See the "Layout" section of the Reflex.Vty.Widget.Layout module documentation.
10− * `tile` no longer takes a configuration record and no longer requires that its child widget return a focus request event. Focus requests are instead handled using calls to `requestFocus` in the child widget.
11− * Calls to `fixed` and `stretch` must now be replaced with `tile . fixed` and `tile . stretch`
12− * `stretch` now takes a minimum size argument
13− * Added `flex` which is equivalent to `stretch 0`
14− * `tabNavigation` no longer returns an `Event`. Instead it calls `requestFocus` directly with the appropriate `Refocus_Shift` value.
15− * Added `axis` (in `MonadLayout`), a lower-level primitive which is used to implement `row` and `col`.
16− * Added `region` (in `MonadLayout`), which is used to claim screen real estate and used to implement `tile` and `grout`
17− * Added `grout`, a container element that is not itself focusable (though its children can be)
18−* Add `HasVtyWidgetCtx` for `pane`-like sub-widget container behavior
19−* Add default instances for `HasVtyInput`, `HasFocus`, and `ImageWriter`
20−* _Breaking Change_: Remove `DynRegion` and `currentRegion`. Use `Dynamic t Region` and `current` instead. This also changes the type of `pane`'s argument.
21−* _Breaking Change_: The following functions are no longer specialized to `VtyWidget`:
22− * `pane`: Now requires `HasVtyWidgetCtx` instead
23− * `drag`: Now requires `HasVtyInput` instead
24− * `mouseDown`: Now requires `HasVtyInput` instead
25− * `mouseUp`: Now requires `HasVtyInput` instead
26− * `mouseScroll`: Now requires `HasVtyInput` instead
27− * `key`: Now requires `HasVtyInput` instead
28− * `keys`: Now requires `HasVtyInput` instead
29− * `keyCombo`: Now requires `HasVtyInput` instead
30− * `keyCombos`: Now requires `HasVtyInput` instead
31− * `splitV`: Now requires `HasVtyWidgetCtx` and `HasDisplaySize` instead
32− * `splitH`: Now requires `HasVtyWidgetCtx` and `HasDisplaySize` instead
33− * `splitVDrag`: Now requires `HasVtyWidgetCtx`, `HasVtyInput`, and `HasDisplaySize` instead
34− * `fill`: Now requires `ImageWriter` and `HasDisplaySize` instead
35− * `boxTitle`: Now requires `HasVtyWidgetCtx`, `ImageWriter`, and `HasDisplaySize` instead
36− * `box`: Now requires `HasVtyWidgetCtx`, `ImageWriter`, and `HasDisplaySize` instead
37− * `boxStatic`: Now requires `HasVtyWidgetCtx`, `ImageWriter`, and `HasDisplaySize` instead
38− * `richText`: Now requires `ImageWriter`, and `HasDisplaySize` instead
39− * `scrollableText`: Now requires `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
40− * `blank`: Now required `Monad` instead
41− * `button`: Now requires `HasFocus`, `HasVtyWidgetCtx`, `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
42− * `textButton`: Now requires `HasFocus`, `HasVtyWidgetCtx`, `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
43− * `textButtonStatic`: Now requires `HasFocus`, `HasVtyWidgetCtx`, `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
44− * `link`: Now requires `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
45− * `checkbox`: Now requires `HasFocus`, `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
46− * `textInput`: Now requires `HasFocus`, `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
47− * `multilineTextInput`: Now requires `HasFocus`, `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
48− * `textInputTile`: Now requires `HasVtyWidgetCtx`, `HasVtyInput`, `MonadLayout`, and `MonadFocus` instead
49−* _Breaking Change_: `CheckboxConfig` now has a field taking an `Event` to set the value of the checkbox.
50−* _Breaking Change_: `checkbox` now accepts keyboard input (spacebar to check and uncheck) and is displayed in bold when focused.
51−* _Breaking Change_: `HasVtyInput` now has a method `localInput` for filtering the input a child widget may receive
52−* Add `anyChildFocused`, which provides information about whether subwidgets are focused
53−* Add `filterKeys`, which is the same as `localInput` but only cares about keyboard events
54−* _Breaking Change_: `ImageWriter` now has a method `mapImages` for transforming the images emitted by a child widget
55−* Add `hoistRunLayout` to apply a transformation to the context of a `Layout` action and run that action
56−* Add an `MFunctor` instance for `VtyWidget`
57−* Bug: fix distribution of available space when it cannot be evenly distributed. Previously, all leftover space would be allocated to the first stretchable widget.
58−* Add a CPU usage indicator to the example executable
59−* _Breaking Change_: `boxTitle` now takes a `Behavior t Text` as its title, instead of a plain `Text`
60−* _Breaking Change_: `fill` now takes a `Behavior t Char` instead of a `Char`
4+
5+* _Module Reorganization_: The following modules have been added (and are all re-exported by Reflex.Vty):
6+ * Reflex.Vty.Widget.Box for all the box functions and datatypes
7+ * Reflex.Vty.Widget.Input.Mouse for clicking, dragging, and scrolling
8+ * Reflex.Vty.Widget.Split contains `splitV`, `splitH`, etc
9+ * Reflex.Vty.Widget.Text contains text rendering functions like `text` and `display`
10+* _Bugfixes_:
11+ * Remove text-icu dependency and switch to `wcwidth` from vty package to compute character width in `Data.Text.Zipper`.
12+ * `goToDisplayLinePosition` in `Data.Text.Zipper` correctly accounts for character width now.
13+ * [#37](https://github.com/reflex-frp/reflex-vty/issues/37) `Layout` should support focus changes through nested layouts (thanks @pdlla for getting this started -- see entry on Layout and Focus below).
14+ * Fix distribution of available space when it cannot be evenly distributed. Previously, all leftover space would be allocated to the first stretchable widget.
15+* _Breaking Changes_:
16+ * Layout and focus have been substantially refactored to fix [#37](https://github.com/reflex-frp/reflex-vty/issues/37) and support a wider variety of layouts and focus switching requirements.
17+ * Added a new `HasFocus` class (the old one is now `HasFocusReader`) to produce focusable elements, and manage focus state. See the "Focus" section of the Reflex.Vty.Widget.Layout module documentation.
18+ * `Layout` no longer has any focus-tracking responsibility. See the "Layout" section of the Reflex.Vty.Widget.Layout module documentation.
19+ * `tile` no longer takes a configuration record and no longer requires that its child widget return a focus request event. Focus requests are instead handled using calls to `requestFocus` in the child widget.
20+ * Calls to `fixed` and `stretch` must now be replaced with `tile . fixed` and `tile . stretch`
21+ * `stretch` now takes a minimum size argument
22+ * Added `flex` which is equivalent to `stretch 0`
23+ * `tabNavigation` no longer returns an `Event`. Instead it calls `requestFocus` directly with the appropriate `Refocus_Shift` value.
24+ * Added `axis` (in `HasLayout`), a lower-level primitive which is used to implement `row` and `col`.
25+ * Added `region` (in `HasLayout`), which is used to claim screen real estate and used to implement `tile` and `grout`
26+ * Added `grout`, a container element that is not itself focusable (though its children can be)
27+ * Removed `VtyWidget` and replaced it with a number of separate classes and monad transformers
28+ * Replace `HasDisplaySize` with `HasDisplayRegion` which carries around a region instead of just a width and height. `displayWidth` and `displayHeight` are now functions implemented in terms of `askRegion` instead of class methods.
29+ * Add a `DisplayRegion` monad transformer
30+ * Rename `ImageWriter` to `HasImageWriter`
31+ * Introduce an `ImageWriter` monad transformer
32+ * Rename `HasFocus` to `HasFocusReader`
33+ * Introduce a `FocusReader` monad transformer
34+ * Replace `HasVtyInput` with `HasInput`
35+ * Introduce an `Input` monad transformer
36+ * Remove `DynRegion` and `currentRegion`. Use `Dynamic t Region` and `current` instead. This also changes the type of `pane`'s argument.
37+ * `CheckboxConfig` now has a field taking an `Event` to set the value of the checkbox.
38+ * `checkbox` now accepts keyboard input (spacebar to check and uncheck) and is displayed in bold when focused.
39+ * `HasInput` (formerly `HasVtyInput`) now has a method `localInput` for filtering the input a child widget may receive
40+ * `HasImageWriter` now has a method `mapImages` for transforming the images emitted by a child widget
41+ * `boxTitle` now takes a `Behavior t Text` as its title, instead of a plain `Text`
42+ * `fill` now takes a `Behavior t Char` instead of a `Char`
43+ * The following functions are no longer specialized to `VtyWidget`:
44+ * `pane`: Now requires `HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m`
45+ * `drag`: Now requires `HasInput`
46+ * `mouseDown`: Now requires `HasInput`
47+ * `mouseUp`: Now requires `HasInput`
48+ * `mouseScroll`: Now requires `HasInput`
49+ * `key`: Now requires `HasInput`
50+ * `keys`: Now requires `HasInput`
51+ * `keyCombo`: Now requires `HasInput`
52+ * `keyCombos`: Now requires `HasInput`
53+ * `splitV`: Now requires `HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m`
54+ * `splitH`: Now requires `HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m`
55+ * `splitVDrag`: Now requires `HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m`
56+ * `fill`: Now requires `HasImageWriter` and `HasDisplayRegion`
57+ * `boxTitle`: Now requires `HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m`
58+ * `box`: Now requires `HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m`
59+ * `boxStatic`: Now requires `HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m`
60+ * `richText`: Now requires `HasImageWriter`, and `HasDisplayRegion`
61+ * `scrollableText`: Now requires `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
62+ * `blank`: Now requires `Monad`
63+ * `button`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
64+ * `textButton`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
65+ * `textButtonStatic`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
66+ * `link`: Now requires `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
67+ * `checkbox`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
68+ * `textInput`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
69+ * `multilineTextInput`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
70+ * `textInputTile`: Now requires `HasFocusReader`, `HasInput`, `HasLayout`, and `HasFocus`
71+* _Misc_:
72+ * Add default instances for `HasInput`, `HasFocus`, and `HasImageWriter`
73+ * Export `withinImage` and add `imagesInRegion` to crop images to a region
74+ * Add `anyChildFocused`, which provides information about whether subwidgets are focused
75+ * Add `filterKeys`, which is the same as `localInput` but only cares about keyboard events
76+ * Add `hoistRunLayout` to apply a transformation to the context of a `Layout` action and run that action
77+ * Add various `MFunctor` instances
78+ * Add a CPU usage indicator to the example executable
6179
6280## 0.1.4.1
6381* Migrate to new dependent-sum / dependent-map (after the "some" package split)
reflex-vty.cabal+4
@@ -23,9 +23,13 @@ library
2323 exposed-modules: Reflex.Vty
2424 , Reflex.Vty.Host
2525 , Reflex.Vty.Widget
26+ , Reflex.Vty.Widget.Box
2627 , Reflex.Vty.Widget.Input
28+ , Reflex.Vty.Widget.Input.Mouse
2729 , Reflex.Vty.Widget.Input.Text
2830 , Reflex.Vty.Widget.Layout
31+ , Reflex.Vty.Widget.Split
32+ , Reflex.Vty.Widget.Text
2933 , Data.Text.Zipper
3034 , Reflex.Class.Switchable
3135 , Reflex.Spider.Orphans
src/Reflex/Vty.hs+6
@@ -14,15 +14,21 @@ module Reflex.Vty
1414 ( module Reflex
1515 , module Reflex.Vty.Host
1616 , module Reflex.Vty.Widget
17+ , module Reflex.Vty.Widget.Box
1718 , module Reflex.Vty.Widget.Input
1819 , module Reflex.Vty.Widget.Layout
20+ , module Reflex.Vty.Widget.Split
21+ , module Reflex.Vty.Widget.Text
1922 , module Control.Monad.NodeId
2023 ) where
2124
2225import Reflex
2326import Reflex.Vty.Host
2427import Reflex.Vty.Widget
28+import Reflex.Vty.Widget.Box
2529import Reflex.Vty.Widget.Input
2630import Reflex.Vty.Widget.Layout
31+import Reflex.Vty.Widget.Split
32+import Reflex.Vty.Widget.Text
2733
2834import Control.Monad.NodeId
src/Reflex/Vty/Widget.hs+258−603
@@ -5,71 +5,17 @@ Description: Basic set of widgets and building blocks for reflex-vty application
55{-# Language UndecidableInstances #-}
66
77module Reflex.Vty.Widget where
8− -- ( VtyWidgetCtx(..)
9− -- , VtyWidget(..)
10− -- , VtyWidgetOut(..)
11− -- , ImageWriter(..)
12− -- , runVtyWidget
13− -- , mainWidget
14− -- , mainWidgetWithHandle
15− -- , HasDisplayRegion(..)
16− -- , HasFocus(..)
17− -- , HasVtyInput(..)
18− -- , HasVtyWidgetCtx(..)
19− -- , Region(..)
20− -- , regionSize
21− -- , regionBlankImage
22− -- , Drag(..)
23− -- , drag
24− -- , MouseDown(..)
25− -- , MouseUp(..)
26− -- , mouseDown
27− -- , mouseUp
28− -- , ScrollDirection(..)
29− -- , mouseScroll
30− -- , pane
31− -- , splitV
32− -- , splitH
33− -- , splitVDrag
34− -- , boxTitle
35− -- , box
36− -- , boxStatic
37− -- , RichTextConfig(..)
38− -- , richText
39− -- , text
40− -- , scrollableText
41− -- , display
42− -- , BoxStyle(..)
43− -- , hyphenBoxStyle
44− -- , singleBoxStyle
45− -- , roundedBoxStyle
46− -- , thickBoxStyle
47− -- , doubleBoxStyle
48− -- , fill
49− -- , hRule
50− -- , filterKeys
51− -- , KeyCombo
52− -- , key
53− -- , keys
54− -- , keyCombo
55− -- , keyCombos
56− -- , blank
57− -- ) where
588
599import Control.Applicative (liftA2)
6010import Control.Monad.Fix (MonadFix)
6111import Control.Monad.IO.Class (MonadIO)
6212import Control.Monad.Morph
6313import Control.Monad.NodeId
64−import Control.Monad.Reader (ReaderT, ask, asks, local, runReaderT)
14+import Control.Monad.Reader (ReaderT, ask, local, runReaderT)
6515import Control.Monad.Ref
6616import Control.Monad.Trans (MonadTrans, lift)
67−import Data.Default (Default(..))
6817import Data.Set (Set)
6918import qualified Data.Set as Set
70−import Data.Text (Text)
71−import qualified Data.Text as T
72−import qualified Data.Text.Zipper as TZ
7319import Graphics.Vty (Image)
7420import qualified Graphics.Vty as V
7521import Reflex
@@ -77,80 +23,9 @@ import Reflex.Class ()
7723import Reflex.Host.Class (MonadReflexCreateTrigger)
7824import Reflex.Vty.Host
7925
80−-- | The context within which a 'VtyWidget' runs
81−data VtyWidgetCtx t = VtyWidgetCtx
82− { _vtyWidgetCtx_focus :: Dynamic t Bool
83− -- ^ Whether the widget should behave as if it has focus for keyboard input.
84− }
26+-- * Running a vty application
8527
86−-- | The output of a 'VtyWidget'
87−data VtyWidgetOut t = VtyWidgetOut
88− { _vtyWidgetOut_shutdown :: Event t ()
89− }
90−
91−instance (Adjustable t m, MonadHold t m, Reflex t) => Adjustable t (VtyWidget t m) where
92− runWithReplace a0 a' = VtyWidget $ runWithReplace (unVtyWidget a0) $ fmap unVtyWidget a'
93− traverseIntMapWithKeyWithAdjust f dm0 dm' = VtyWidget $
94− traverseIntMapWithKeyWithAdjust (\k v -> unVtyWidget (f k v)) dm0 dm'
95− traverseDMapWithKeyWithAdjust f dm0 dm' = VtyWidget $ do
96− traverseDMapWithKeyWithAdjust (\k v -> unVtyWidget (f k v)) dm0 dm'
97− traverseDMapWithKeyWithAdjustWithMove f dm0 dm' = VtyWidget $ do
98− traverseDMapWithKeyWithAdjustWithMove (\k v -> unVtyWidget (f k v)) dm0 dm'
99−
100−-- | A widget that can read its context and produce image output
101−newtype VtyWidget t m a = VtyWidget
102− { unVtyWidget ::
103− ReaderT (VtyWidgetCtx t)
104− (ReaderT (Event t VtyEvent) m) a
105− } deriving
106− ( Functor
107− , Applicative
108− , Monad
109− , MonadSample t
110− , MonadHold t
111− , MonadFix
112− , MonadIO
113− , MonadRef
114− )
115−
116−deriving instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (VtyWidget t m)
117−deriving instance NotReady t m => NotReady t (VtyWidget t m)
118−deriving instance PerformEvent t m => PerformEvent t (VtyWidget t m)
119−deriving instance PostBuild t m => PostBuild t (VtyWidget t m)
120−deriving instance TriggerEvent t m => TriggerEvent t (VtyWidget t m)
121−instance HasImageWriter t m => HasImageWriter t (VtyWidget t m)
122−instance HasDisplayRegion t m => HasDisplayRegion t (VtyWidget t m)
123−
124−instance MonadTrans (VtyWidget t) where
125− lift f = VtyWidget $ lift $ lift f
126−
127−instance MFunctor (VtyWidget t) where
128− hoist f = VtyWidget . hoist (hoist f) . unVtyWidget
129−
130−instance MonadNodeId m => MonadNodeId (VtyWidget t m)
131−
132−
133−-- | A reader-like class for the vty widget context. Allows actions to be run in a sub-context.
134−class HasVtyWidgetCtx t m | m -> t where
135− askCtx :: m (VtyWidgetCtx t)
136− default askCtx :: (f m' ~ m, Monad m', MonadTrans f, HasVtyWidgetCtx t m') => m (VtyWidgetCtx t)
137− askCtx = lift askCtx
138− localCtx :: (VtyWidgetCtx t -> VtyWidgetCtx t) -> m a -> m a
139−
140−instance (Monad m, Reflex t) => HasVtyWidgetCtx t (VtyWidget t m) where
141− askCtx = VtyWidget $ ask
142− localCtx f (VtyWidget x) = VtyWidget $ local f x
143−
144−-- | Runs a 'VtyWidget' with a given context
145−runVtyWidget
146− :: (Reflex t, MonadNodeId m)
147− => Event t VtyEvent
148− -> VtyWidgetCtx t
149− -> VtyWidget t m a
150− -> m a
151−runVtyWidget e ctx w = runReaderT (runReaderT (unVtyWidget w) ctx) e
152−
153−-- | Sets up the top-level context for a 'VtyWidget' and runs it with that context
28+-- | Sets up the top-level context for a vty widget and runs it with that context
15429mainWidgetWithHandle
15530 :: V.Vty
15631 -> (forall t m.
@@ -158,7 +33,9 @@ mainWidgetWithHandle
15833 , HasImageWriter t m
15934 , MonadNodeId m
16035 , HasDisplayRegion t m
161− ) => VtyWidget t m (Event t ()))
36+ , HasFocusReader t m
37+ , HasInput t m
38+ ) => m (Event t ()))
16239 -> IO ()
16340mainWidgetWithHandle vty child =
16441 runVtyAppWithHandle vty $ \dr0 inp -> do
@@ -168,13 +45,10 @@ mainWidgetWithHandle vty child =
16845 let inp' = fforMaybe inp $ \case
16946 V.EvResize {} -> Nothing
17047 x -> Just x
171− let ctx = VtyWidgetCtx
172− { _vtyWidgetCtx_focus = constDyn True
173− }
174− (shutdown, images) <- runDisplayRegion (fmap (\(w, h) -> Region 0 0 w h) size) $
48+ (shutdown, images) <- runFocusReader (pure True) $ runDisplayRegion (fmap (\(w, h) -> Region 0 0 w h) size) $
17549 runImageWriter $
17650 runNodeIdT $
177− runVtyWidget inp' ctx $ do
51+ runInput inp' $ do
17852 tellImages . ffor (current size) $ \(w, h) -> [V.charFill V.defAttr ' ' w h]
17953 child
18054 return $ VtyResult
@@ -182,6 +56,11 @@ mainWidgetWithHandle vty child =
18256 , _vtyResult_shutdown = shutdown
18357 }
18458
59+-- | The output of a vty widget
60+data VtyWidgetOut t = VtyWidgetOut
61+ { _vtyWidgetOut_shutdown :: Event t ()
62+ }
63+
18564-- | Like 'mainWidgetWithHandle', but uses a default vty configuration
18665mainWidget
18766 :: (forall t m.
@@ -189,12 +68,167 @@ mainWidget
18968 , HasImageWriter t m
19069 , MonadNodeId m
19170 , HasDisplayRegion t m
192− ) => VtyWidget t m (Event t ()))
71+ , HasFocusReader t m
72+ , HasInput t m
73+ ) => m (Event t ()))
19374 -> IO ()
19475mainWidget child = do
19576 vty <- getDefaultVty
19677 mainWidgetWithHandle vty child
19778
79+-- * Input Events
80+
81+-- | A class for things that can receive vty events as input
82+class HasInput t m | m -> t where
83+ input :: m (Event t VtyEvent)
84+ default input :: (f m' ~ m, Monad m', MonadTrans f, HasInput t m') => m (Event t VtyEvent)
85+ input = lift input
86+ -- | User input events that the widget's parent chooses to share. These will generally
87+ -- be filtered for relevance.
88+ localInput :: (Event t VtyEvent -> Event t VtyEvent) -> m a -> m a
89+ default localInput :: (f m' ~ m, Monad m', MFunctor f, HasInput t m') => (Event t VtyEvent -> Event t VtyEvent) -> m a -> m a
90+ localInput f = hoist (localInput f)
91+
92+instance (Reflex t, Monad m) => HasInput t (Input t m) where
93+ input = Input ask
94+ localInput f (Input m) = Input $ local f m
95+
96+-- | A widget that can receive input events. See 'Graphics.Vty.Event'
97+newtype Input t m a = Input
98+ { unInput :: ReaderT (Event t VtyEvent) m a
99+ } deriving
100+ ( Functor
101+ , Applicative
102+ , Monad
103+ , MonadSample t
104+ , MonadHold t
105+ , MonadFix
106+ , MonadIO
107+ , MonadRef
108+ )
109+
110+instance (Adjustable t m, MonadHold t m, Reflex t) => Adjustable t (Input t m) where
111+ runWithReplace a0 a' = Input $ runWithReplace (unInput a0) $ fmap unInput a'
112+ traverseIntMapWithKeyWithAdjust f dm0 dm' = Input $
113+ traverseIntMapWithKeyWithAdjust (\k v -> unInput (f k v)) dm0 dm'
114+ traverseDMapWithKeyWithAdjust f dm0 dm' = Input $ do
115+ traverseDMapWithKeyWithAdjust (\k v -> unInput (f k v)) dm0 dm'
116+ traverseDMapWithKeyWithAdjustWithMove f dm0 dm' = Input $ do
117+ traverseDMapWithKeyWithAdjustWithMove (\k v -> unInput (f k v)) dm0 dm'
118+
119+deriving instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (Input t m)
120+deriving instance NotReady t m => NotReady t (Input t m)
121+deriving instance PerformEvent t m => PerformEvent t (Input t m)
122+deriving instance PostBuild t m => PostBuild t (Input t m)
123+deriving instance TriggerEvent t m => TriggerEvent t (Input t m)
124+instance HasImageWriter t m => HasImageWriter t (Input t m)
125+instance HasDisplayRegion t m => HasDisplayRegion t (Input t m)
126+instance HasFocusReader t m => HasFocusReader t (Input t m)
127+
128+instance MonadTrans (Input t) where
129+ lift f = Input $ lift f
130+
131+instance MFunctor (Input t) where
132+ hoist f = Input . hoist f . unInput
133+
134+instance MonadNodeId m => MonadNodeId (Input t m)
135+
136+-- | Runs an 'Input' with a given context
137+runInput
138+ :: Reflex t
139+ => Event t VtyEvent
140+ -> Input t m a
141+ -> m a
142+runInput e w = runReaderT (unInput w) e
143+
144+-- ** Filtering input
145+
146+-- | Type synonym for a key and modifier combination
147+type KeyCombo = (V.Key, [V.Modifier])
148+
149+-- | Emits an event that fires on a particular key press (without modifiers)
150+key :: (Monad m, Reflex t, HasInput t m) => V.Key -> m (Event t KeyCombo)
151+key = keyCombos . Set.singleton . (,[])
152+
153+-- | Emits an event that fires on particular key presses (without modifiers)
154+keys :: (Monad m, Reflex t, HasInput t m) => [V.Key] -> m (Event t KeyCombo)
155+keys = keyCombos . Set.fromList . fmap (,[])
156+
157+-- | Emit an event that fires whenever the provided key combination occurs
158+keyCombo
159+ :: (Reflex t, Monad m, HasInput t m)
160+ => KeyCombo
161+ -> m (Event t KeyCombo)
162+keyCombo = keyCombos . Set.singleton
163+
164+-- | Emit an event that fires whenever any of the provided key combinations occur
165+keyCombos
166+ :: (Reflex t, Monad m, HasInput t m)
167+ => Set KeyCombo
168+ -> m (Event t KeyCombo)
169+keyCombos ks = do
170+ i <- input
171+ return $ fforMaybe i $ \case
172+ V.EvKey k m -> if Set.member (k, m) ks
173+ then Just (k, m)
174+ else Nothing
175+ _ -> Nothing
176+
177+-- | Filter the keyboard input that a child widget may receive
178+filterKeys :: (Reflex t, HasInput t m) => (KeyCombo -> Bool) -> m a -> m a
179+filterKeys f x = localInput (ffilter (\case
180+ V.EvKey k mods -> f (k, mods)
181+ _ -> True)) x
182+
183+-- | Filter mouse input events based on whether they target a particular region
184+-- and translate them to the internal coordinate system of that region.
185+--
186+-- NB: Non-mouse events are passed through unfiltered and unchanged
187+mouseInRegion :: Region -> VtyEvent -> Maybe VtyEvent
188+mouseInRegion (Region l t w h) e = case e of
189+ V.EvMouseDown x y btn m -> mouse (\u v -> V.EvMouseDown u v btn m) x y
190+ V.EvMouseUp x y btn -> mouse (\u v -> V.EvMouseUp u v btn) x y
191+ _ -> Just e
192+ where
193+ mouse con x y
194+ | or [ x < l
195+ , y < t
196+ , x >= l + w
197+ , y >= t + h ] = Nothing
198+ | otherwise =
199+ Just (con (x - l) (y - t))
200+
201+-- | Filter mouse input outside the current display region and
202+-- all input if the region is not focused
203+inputInFocusedRegion
204+ :: (HasDisplayRegion t m, HasFocusReader t m, HasInput t m)
205+ => m (Event t VtyEvent)
206+inputInFocusedRegion = do
207+ inp <- input
208+ reg <- current <$> askRegion
209+ f <- current <$> focus
210+ pure $ gate f $ fmapMaybe id $ attachWith mouseInRegion reg inp
211+
212+-- * Getting and setting the display region
213+
214+-- | A chunk of the display area
215+data Region = Region
216+ { _region_left :: Int
217+ , _region_top :: Int
218+ , _region_width :: Int
219+ , _region_height :: Int
220+ }
221+ deriving (Show, Read, Eq, Ord)
222+
223+-- | The width and height of a 'Region'
224+regionSize :: Region -> (Int, Int)
225+regionSize (Region _ _ w h) = (w, h)
226+
227+-- | Produces an 'Image' that fills a region with space characters
228+regionBlankImage :: Region -> Image
229+regionBlankImage r@(Region _ _ width height) =
230+ withinImage r $ V.charFill V.defAttr ' ' width height
231+
198232-- | A class for things that know their own display size dimensions
199233class (Reflex t, Monad m) => HasDisplayRegion t m | m -> t where
200234 -- | Retrieve the display region
@@ -206,9 +240,11 @@ class (Reflex t, Monad m) => HasDisplayRegion t m | m -> t where
206240 default localRegion :: (f m' ~ m, Monad m', MFunctor f, HasDisplayRegion t m') => (Dynamic t Region -> Dynamic t Region) -> m a -> m a
207241 localRegion f = hoist (localRegion f)
208242
243+-- | Retrieve the display width
209244displayWidth :: HasDisplayRegion t m => m (Dynamic t Int)
210245displayWidth = fmap _region_width <$> askRegion
211246
247+-- | Retrieve the display height
212248displayHeight :: HasDisplayRegion t m => m (Dynamic t Int)
213249displayHeight = fmap _region_height <$> askRegion
214250
@@ -218,7 +254,9 @@ instance HasDisplayRegion t m => HasDisplayRegion t (DynamicWriterT t x m)
218254instance HasDisplayRegion t m => HasDisplayRegion t (EventWriterT t x m)
219255instance HasDisplayRegion t m => HasDisplayRegion t (NodeIdT m)
220256
221−newtype DisplayRegion t m a = DisplayRegion { unDisplayRegion :: ReaderT (Dynamic t Region) m a }
257+-- | A widget that has access to a particular region of the vty display
258+newtype DisplayRegion t m a = DisplayRegion
259+ { unDisplayRegion :: ReaderT (Dynamic t Region) m a }
222260 deriving
223261 ( Functor
224262 , Applicative
@@ -240,6 +278,7 @@ deriving instance PerformEvent t m => PerformEvent t (DisplayRegion t m)
240278deriving instance PostBuild t m => PostBuild t (DisplayRegion t m)
241279deriving instance TriggerEvent t m => TriggerEvent t (DisplayRegion t m)
242280instance HasImageWriter t m => HasImageWriter t (DisplayRegion t m)
281+instance HasFocusReader t m => HasFocusReader t (DisplayRegion t m)
243282
244283instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (DisplayRegion t m) where
245284 runWithReplace (DisplayRegion a) e = DisplayRegion $ runWithReplace a $ fmap unDisplayRegion e
@@ -247,7 +286,6 @@ instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (DisplayReg
247286 traverseDMapWithKeyWithAdjust f m e = DisplayRegion $ traverseDMapWithKeyWithAdjust (\k v -> unDisplayRegion $ f k v) m e
248287 traverseDMapWithKeyWithAdjustWithMove f m e = DisplayRegion $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unDisplayRegion $ f k v) m e
249288
250−
251289instance MonadTrans (DisplayRegion t) where
252290 lift = DisplayRegion . lift
253291
@@ -256,6 +294,7 @@ instance MFunctor (DisplayRegion t) where
256294
257295instance MonadNodeId m => MonadNodeId (DisplayRegion t m)
258296
297+-- | Run a 'DisplayRegion' action with a given 'Region'
259298runDisplayRegion
260299 :: (Reflex t, Monad m)
261300 => Dynamic t Region
@@ -263,33 +302,71 @@ runDisplayRegion
263302 -> m a
264303runDisplayRegion r = flip runReaderT r . unDisplayRegion
265304
266−-- | A class for things that can receive vty events as input
267−class HasVtyInput t m | m -> t where
268− input :: m (Event t VtyEvent)
269− default input :: (f m' ~ m, Monad m', MonadTrans f, HasVtyInput t m') => m (Event t VtyEvent)
270− input = lift input
271− -- | User input events that the widget's parent chooses to share. These will generally
272− -- be filtered for relevance.
273− localInput :: (Event t VtyEvent -> Event t VtyEvent) -> m a -> m a
274−
275−-- | Filter the keyboard input that a child widget may receive
276−filterKeys :: (Reflex t, HasVtyInput t m) => (KeyCombo -> Bool) -> m a -> m a
277−filterKeys f x = localInput (ffilter (\case
278− V.EvKey k mods -> f (k, mods)
279− _ -> True)) x
280−
281−instance (Reflex t, Monad m) => HasVtyInput t (VtyWidget t m) where
282− input = VtyWidget . lift $ ask
283− localInput f (VtyWidget m) = VtyWidget $ hoist (local f) m
305+-- * Getting focus state
284306
285307-- | A class for things that can dynamically gain and lose focus
286−class HasFocus t m | m -> t where
308+class (Reflex t, Monad m) => HasFocusReader t m | m -> t where
287309 focus :: m (Dynamic t Bool)
288− default focus :: (f m' ~ m, Monad m', MonadTrans f, HasFocus t m') => m (Dynamic t Bool)
310+ default focus :: (f m' ~ m, Monad m', MonadTrans f, HasFocusReader t m') => m (Dynamic t Bool)
289311 focus = lift focus
312+ localFocus :: (Dynamic t Bool -> Dynamic t Bool) -> m a -> m a
313+ default localFocus :: (f m' ~ m, Monad m', MFunctor f, HasFocusReader t m') => (Dynamic t Bool -> Dynamic t Bool) -> m a -> m a
314+ localFocus f = hoist (localFocus f)
315+
316+instance HasFocusReader t m => HasFocusReader t (ReaderT x m)
317+instance HasFocusReader t m => HasFocusReader t (BehaviorWriterT t x m)
318+instance HasFocusReader t m => HasFocusReader t (DynamicWriterT t x m)
319+instance HasFocusReader t m => HasFocusReader t (EventWriterT t x m)
320+instance HasFocusReader t m => HasFocusReader t (NodeIdT m)
321+
322+-- | A widget that has access to information about whether it is focused
323+newtype FocusReader t m a = FocusReader
324+ { unFocusReader :: ReaderT (Dynamic t Bool) m a }
325+ deriving
326+ ( Functor
327+ , Applicative
328+ , Monad
329+ , MonadFix
330+ , MonadHold t
331+ , MonadIO
332+ , MonadRef
333+ , MonadSample t
334+ )
335+
336+instance (Monad m, Reflex t) => HasFocusReader t (FocusReader t m) where
337+ focus = FocusReader ask
338+ localFocus f = FocusReader . local f . unFocusReader
339+
340+deriving instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (FocusReader t m)
341+deriving instance NotReady t m => NotReady t (FocusReader t m)
342+deriving instance PerformEvent t m => PerformEvent t (FocusReader t m)
343+deriving instance PostBuild t m => PostBuild t (FocusReader t m)
344+deriving instance TriggerEvent t m => TriggerEvent t (FocusReader t m)
345+instance HasImageWriter t m => HasImageWriter t (FocusReader t m)
346+
347+instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (FocusReader t m) where
348+ runWithReplace (FocusReader a) e = FocusReader $ runWithReplace a $ fmap unFocusReader e
349+ traverseIntMapWithKeyWithAdjust f m e = FocusReader $ traverseIntMapWithKeyWithAdjust (\k v -> unFocusReader $ f k v) m e
350+ traverseDMapWithKeyWithAdjust f m e = FocusReader $ traverseDMapWithKeyWithAdjust (\k v -> unFocusReader $ f k v) m e
351+ traverseDMapWithKeyWithAdjustWithMove f m e = FocusReader $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unFocusReader $ f k v) m e
290352
291−instance (Reflex t, Monad m) => HasFocus t (VtyWidget t m) where
292− focus = VtyWidget $ asks _vtyWidgetCtx_focus
353+instance MonadTrans (FocusReader t) where
354+ lift = FocusReader . lift
355+
356+instance MFunctor (FocusReader t) where
357+ hoist f = FocusReader . hoist f . unFocusReader
358+
359+instance MonadNodeId m => MonadNodeId (FocusReader t m)
360+
361+-- | Run a 'FocusReader' action with the given focus value
362+runFocusReader
363+ :: (Reflex t, Monad m)
364+ => Dynamic t Bool
365+ -> FocusReader t m a
366+ -> m a
367+runFocusReader b = flip runReaderT b . unFocusReader
368+
369+-- * "Image" output
293370
294371-- | A class for widgets that can produce images to draw to the display
295372class (Reflex t, Monad m) => HasImageWriter t m | m -> t where
@@ -302,7 +379,9 @@ class (Reflex t, Monad m) => HasImageWriter t m | m -> t where
302379 default mapImages :: (f m' ~ m, Monad m', MFunctor f, HasImageWriter t m') => (Behavior t [Image] -> Behavior t [Image]) -> m a -> m a
303380 mapImages f = hoist (mapImages f)
304381
305−newtype ImageWriter t m a = ImageWriter { unImageWriter :: BehaviorWriterT t [Image] m a }
382+-- | A widget that can produce images to draw onto the display
383+newtype ImageWriter t m a = ImageWriter
384+ { unImageWriter :: BehaviorWriterT t [Image] m a }
306385 deriving
307386 ( Functor
308387 , Applicative
@@ -345,30 +424,16 @@ instance (Monad m, Reflex t) => HasImageWriter t (ImageWriter t m) where
345424 pure a
346425
347426instance HasDisplayRegion t m => HasDisplayRegion t (ImageWriter t m)
427+instance HasFocusReader t m => HasFocusReader t (ImageWriter t m)
348428
429+-- | Run a widget that can produce images
349430runImageWriter
350431 :: (Reflex t, Monad m)
351432 => ImageWriter t m a
352433 -> m (a, Behavior t [Image])
353434runImageWriter = runBehaviorWriterT . unImageWriter
354435
355−-- | A chunk of the display area
356−data Region = Region
357− { _region_left :: Int
358− , _region_top :: Int
359− , _region_width :: Int
360− , _region_height :: Int
361− }
362− deriving (Show, Read, Eq, Ord)
363−
364−-- | The width and height of a 'Region'
365−regionSize :: Region -> (Int, Int)
366−regionSize (Region _ _ w h) = (w, h)
367−
368−-- | Produces an 'Image' that fills a region with space characters
369−regionBlankImage :: Region -> Image
370−regionBlankImage r@(Region _ _ width height) =
371− withinImage r $ V.charFill V.defAttr ' ' width height
436+-- ** Manipulating images
372437
373438-- | Translates and crops an 'Image' so that it is contained by
374439-- the given 'Region'.
@@ -380,7 +445,17 @@ withinImage (Region left top width height)
380445 | width < 0 || height < 0 = withinImage (Region left top 0 0)
381446 | otherwise = V.translate left top . V.crop width height
382447
383−-- | Low-level widget combinator that runs a child 'VtyWidget' within
448+-- | Crop a behavior of images to a behavior of regions. See 'withinImage'.
449+imagesInRegion
450+ :: Reflex t
451+ => Behavior t Region
452+ -> Behavior t [Image]
453+ -> Behavior t [Image]
454+imagesInRegion reg = liftA2 (\r is -> map (withinImage r) is) reg
455+
456+-- * Running sub-widgets
457+
458+-- | Low-level widget combinator that runs a child 'Input' within
384459-- a given region and context. This widget filters and modifies the input
385460-- that the child widget receives such that:
386461-- * unfocused widgets receive no key events
@@ -388,435 +463,15 @@ withinImage (Region left top width height)
388463-- * mouse inputs inside the region have their coordinates translated such
389464-- that (0,0) is the top-left corner of the region
390465pane
391− :: (Reflex t, Monad m, MonadNodeId m, HasVtyWidgetCtx t m, HasVtyInput t m, HasImageWriter t m, HasDisplayRegion t m)
466+ :: (Reflex t, Monad m, MonadNodeId m, HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
392467 => Dynamic t Region
393468 -> Dynamic t Bool -- ^ Whether the widget should be focused when the parent is.
394469 -> m a
395470 -> m a
396−pane dr foc child = do
397− let reg = current dr
398− let subContext ctx = VtyWidgetCtx
399− { _vtyWidgetCtx_focus = liftA2 (&&) (_vtyWidgetCtx_focus ctx) foc
400− }
401− inputFilter = fmapMaybe id .
402− attachWith (\(r,f) e -> filterInput r f e)
403− (liftA2 (,) reg (current foc))
404− let imagesWithinRegion images = liftA2 (\is r -> map (withinImage r) is) images reg
405− localRegion (const dr) $ mapImages imagesWithinRegion $ localInput inputFilter $ localCtx subContext child
406− where
407− filterInput :: Region -> Bool -> VtyEvent -> Maybe VtyEvent
408− filterInput (Region l t w h) focused e = case e of
409− V.EvKey _ _ | not focused -> Nothing
410− V.EvMouseDown x y btn m -> mouse (\u v -> V.EvMouseDown u v btn m) x y
411− V.EvMouseUp x y btn -> mouse (\u v -> V.EvMouseUp u v btn) x y
412− _ -> Just e
413− where
414− mouse con x y
415− | or [ x < l
416− , y < t
417− , x >= l + w
418− , y >= t + h ] = Nothing
419− | otherwise =
420− Just (con (x - l) (y - t))
421−
422−-- | Information about a drag operation
423−data Drag = Drag
424− { _drag_from :: (Int, Int) -- ^ Where the drag began
425− , _drag_to :: (Int, Int) -- ^ Where the mouse currently is
426− , _drag_button :: V.Button -- ^ Which mouse button is dragging
427− , _drag_modifiers :: [V.Modifier] -- ^ What modifiers are held
428− , _drag_end :: Bool -- ^ Whether the drag ended (the mouse button was released)
429− }
430− deriving (Eq, Ord, Show)
431−
432−-- | Converts raw vty mouse drag events into an event stream of 'Drag's
433−drag
434− :: (Reflex t, MonadFix m, MonadHold t m, HasVtyInput t m)
435− => V.Button
436− -> m (Event t Drag)
437−drag btn = do
438− inp <- input
439− let f :: Maybe Drag -> V.Event -> Maybe Drag
440− f Nothing = \case
441− V.EvMouseDown x y btn' mods
442− | btn == btn' -> Just $ Drag (x,y) (x,y) btn' mods False
443− | otherwise -> Nothing
444− _ -> Nothing
445− f (Just (Drag from _ _ mods end)) = \case
446− V.EvMouseDown x y btn' mods'
447− | end && btn == btn' -> Just $ Drag (x,y) (x,y) btn' mods' False
448− | btn == btn' -> Just $ Drag from (x,y) btn mods' False
449− | otherwise -> Nothing -- Ignore other buttons.
450− V.EvMouseUp x y (Just btn')
451− | end -> Nothing
452− | btn == btn' -> Just $ Drag from (x,y) btn mods True
453− | otherwise -> Nothing
454− V.EvMouseUp x y Nothing -- Terminal doesn't specify mouse up button,
455− -- assume it's the right one.
456− | end -> Nothing
457− | otherwise -> Just $ Drag from (x,y) btn mods True
458− _ -> Nothing
459− rec let newDrag = attachWithMaybe f (current dragD) inp
460− dragD <- holdDyn Nothing $ Just <$> newDrag
461− return (fmapMaybe id $ updated dragD)
462−
463−-- | Mouse down events for a particular mouse button
464−mouseDown
465− :: (Reflex t, Monad m, HasVtyInput t m)
466− => V.Button
467− -> m (Event t MouseDown)
468−mouseDown btn = do
469− i <- input
470− return $ fforMaybe i $ \case
471− V.EvMouseDown x y btn' mods -> if btn == btn'
472− then Just $ MouseDown btn' (x, y) mods
473− else Nothing
474− _ -> Nothing
475−
476−-- | Mouse up events for a particular mouse button
477−mouseUp
478− :: (Reflex t, Monad m, HasVtyInput t m)
479− => m (Event t MouseUp)
480−mouseUp = do
481− i <- input
482− return $ fforMaybe i $ \case
483− V.EvMouseUp x y btn' -> Just $ MouseUp btn' (x, y)
484− _ -> Nothing
485−
486−-- | Information about a mouse down event
487−data MouseDown = MouseDown
488− { _mouseDown_button :: V.Button
489− , _mouseDown_coordinates :: (Int, Int)
490− , _mouseDown_modifiers :: [V.Modifier]
491− }
492− deriving (Eq, Ord, Show)
493−
494−-- | Information about a mouse up event
495−data MouseUp = MouseUp
496− { _mouseUp_button :: Maybe V.Button
497− , _mouseUp_coordinates :: (Int, Int)
498− }
499− deriving (Eq, Ord, Show)
500−
501−-- | Mouse scroll direction
502−data ScrollDirection = ScrollDirection_Up | ScrollDirection_Down
503− deriving (Eq, Ord, Show)
504−
505−-- | Produce an event that fires when the mouse wheel is scrolled
506−mouseScroll
507− :: (Reflex t, Monad m, HasVtyInput t m)
508− => m (Event t ScrollDirection)
509−mouseScroll = do
510− up <- mouseDown V.BScrollUp
511− down <- mouseDown V.BScrollDown
512− return $ leftmost
513− [ ScrollDirection_Up <$ up
514− , ScrollDirection_Down <$ down
515− ]
516−
517−-- | Type synonym for a key and modifier combination
518−type KeyCombo = (V.Key, [V.Modifier])
519−
520−-- | Emits an event that fires on a particular key press (without modifiers)
521−key :: (Monad m, Reflex t, HasVtyInput t m) => V.Key -> m (Event t KeyCombo)
522−key = keyCombos . Set.singleton . (,[])
523−
524−-- | Emits an event that fires on particular key presses (without modifiers)
525−keys :: (Monad m, Reflex t, HasVtyInput t m) => [V.Key] -> m (Event t KeyCombo)
526−keys = keyCombos . Set.fromList . fmap (,[])
527−
528−-- | Emit an event that fires whenever the provided key combination occurs
529−keyCombo
530− :: (Reflex t, Monad m, HasVtyInput t m)
531− => KeyCombo
532− -> m (Event t KeyCombo)
533−keyCombo = keyCombos . Set.singleton
534−
535−-- | Emit an event that fires whenever any of the provided key combinations occur
536−keyCombos
537− :: (Reflex t, Monad m, HasVtyInput t m)
538− => Set KeyCombo
539− -> m (Event t KeyCombo)
540−keyCombos ks = do
541− i <- input
542− return $ fforMaybe i $ \case
543− V.EvKey k m -> if Set.member (k, m) ks
544− then Just (k, m)
545− else Nothing
546− _ -> Nothing
547−
548−-- | A plain split of the available space into vertically stacked panes.
549−-- No visual separator is built in here.
550−splitV :: (Reflex t, Monad m, MonadNodeId m, HasVtyWidgetCtx t m, HasDisplayRegion t m, HasVtyInput t m, HasImageWriter t m)
551− => Dynamic t (Int -> Int)
552− -- ^ Function used to determine size of first pane based on available size
553− -> Dynamic t (Bool, Bool)
554− -- ^ How to focus the two sub-panes, given that we are focused.
555− -> m a
556− -- ^ Widget for first pane
557− -> m b
558− -- ^ Widget for second pane
559− -> m (a,b)
560−splitV sizeFunD focD wA wB = do
561− dw <- displayWidth
562− dh <- displayHeight
563− let regA = Region 0 0 <$> dw <*> (sizeFunD <*> dh)
564− regB = Region 0 <$> (_region_height <$> regA) <*> dw <*> liftA2 (-) dh (_region_height <$> regA)
565− ra <- pane regA (fst <$> focD) wA
566− rb <- pane regB (snd <$> focD) wB
567− return (ra,rb)
568−
569−-- | A plain split of the available space into horizontally stacked panes.
570−-- No visual separator is built in here.
571−splitH :: (Reflex t, Monad m, MonadNodeId m, HasDisplayRegion t m, HasVtyWidgetCtx t m, HasVtyInput t m, HasImageWriter t m)
572− => Dynamic t (Int -> Int)
573− -- ^ Function used to determine size of first pane based on available size
574− -> Dynamic t (Bool, Bool)
575− -- ^ How to focus the two sub-panes, given that we are focused.
576− -> m a
577− -- ^ Widget for first pane
578− -> m b
579− -- ^ Widget for second pane
580− -> m (a,b)
581−splitH sizeFunD focD wA wB = do
582− dw <- displayWidth
583− dh <- displayHeight
584− let regA = Region 0 0 <$> (sizeFunD <*> dw) <*> dh
585− regB = Region <$> (_region_width <$> regA) <*> 0 <*> liftA2 (-) dw (_region_width <$> regA) <*> dh
586− liftA2 (,) (pane regA (fmap fst focD) wA) (pane regB (fmap snd focD) wB)
587−
588−-- | A split of the available space into two parts with a draggable separator.
589−-- Starts with half the space allocated to each, and the first pane has focus.
590−-- Clicking in a pane switches focus.
591−splitVDrag :: (Reflex t, MonadFix m, MonadHold t m, MonadNodeId m, HasDisplayRegion t m, HasVtyInput t m, HasVtyWidgetCtx t m, HasImageWriter t m)
592− => m ()
593− -> m a
594− -> m b
595− -> m (a,b)
596−splitVDrag wS wA wB = do
597− dh <- displayHeight
598− dw <- displayWidth
599− h0 <- sample $ current dh -- TODO
600− dragE <- drag V.BLeft
601− let splitter0 = h0 `div` 2
602− rec splitterCheckpoint <- holdDyn splitter0 $ leftmost [fst <$> ffilter snd dragSplitter, resizeSplitter]
603− splitterPos <- holdDyn splitter0 $ leftmost [fst <$> dragSplitter, resizeSplitter]
604− splitterFrac <- holdDyn ((1::Double) / 2) $ ffor (attach (current dh) (fst <$> dragSplitter)) $ \(h, x) ->
605− fromIntegral x / max 1 (fromIntegral h)
606− let dragSplitter = fforMaybe (attach (current splitterCheckpoint) dragE) $
607− \(splitterY, Drag (_, fromY) (_, toY) _ _ end) ->
608− if splitterY == fromY then Just (toY, end) else Nothing
609− regA = Region 0 0 <$> dw <*> splitterPos
610− regS = Region 0 <$> splitterPos <*> dw <*> 1
611− regB = Region 0 <$> (splitterPos + 1) <*> dw <*> (dh - splitterPos - 1)
612− resizeSplitter = ffor (attach (current splitterFrac) (updated dh)) $
613− \(frac, h) -> round (frac * fromIntegral h)
614− focA <- holdDyn True $ leftmost
615− [ True <$ mA
616− , False <$ mB
617− ]
618− (mA, rA) <- pane regA focA $ withMouseDown wA
619− pane regS (pure False) wS
620− (mB, rB) <- pane regB (not <$> focA) $ withMouseDown wB
621− return (rA, rB)
622− where
623− withMouseDown x = do
624− m <- mouseDown V.BLeft
625− x' <- x
626− return (m, x')
627−
628−-- | Fill the background with a particular character.
629−fill :: (HasDisplayRegion t m, HasImageWriter t m) => Behavior t Char -> m ()
630−fill bc = do
631− dw <- displayWidth
632− dh <- displayHeight
633− let fillImg =
634− (\w h c -> [V.charFill V.defAttr c w h])
635− <$> current dw
636− <*> current dh
637− <*> bc
638− tellImages fillImg
639−
640−-- | Fill the background with the bottom box style
641−hRule :: (HasDisplayRegion t m, HasImageWriter t m) => BoxStyle -> m ()
642−hRule boxStyle = fill $ pure (_boxStyle_s boxStyle)
643−
644−-- | Defines a set of symbols to use to draw the outlines of boxes
645−-- C.f. https://en.wikipedia.org/wiki/Box-drawing_character
646−data BoxStyle = BoxStyle
647− { _boxStyle_nw :: Char
648− , _boxStyle_n :: Char
649− , _boxStyle_ne :: Char
650− , _boxStyle_e :: Char
651− , _boxStyle_se :: Char
652− , _boxStyle_s :: Char
653− , _boxStyle_sw :: Char
654− , _boxStyle_w :: Char
655− }
656−
657−instance Default BoxStyle where
658− def = singleBoxStyle
659−
660−-- | A box style that uses hyphens and pipe characters. Doesn't handle
661−-- corners very well.
662−hyphenBoxStyle :: BoxStyle
663−hyphenBoxStyle = BoxStyle '-' '-' '-' '|' '-' '-' '-' '|'
664−
665−-- | A single line box style
666−singleBoxStyle :: BoxStyle
667−singleBoxStyle = BoxStyle '┌' '─' '┐' '│' '┘' '─' '└' '│'
668−
669−-- | A thick single line box style
670−thickBoxStyle :: BoxStyle
671−thickBoxStyle = BoxStyle '┏' '━' '┓' '┃' '┛' '━' '┗' '┃'
672−
673−-- | A double line box style
674−doubleBoxStyle :: BoxStyle
675−doubleBoxStyle = BoxStyle '╔' '═' '╗' '║' '╝' '═' '╚' '║'
676−
677−-- | A single line box style with rounded corners
678−roundedBoxStyle :: BoxStyle
679−roundedBoxStyle = BoxStyle '╭' '─' '╮' '│' '╯' '─' '╰' '│'
680−
681−-- | Draws a titled box in the provided style and a child widget inside of that box
682−boxTitle :: (Monad m, Reflex t, MonadNodeId m, HasDisplayRegion t m, HasImageWriter t m, HasVtyWidgetCtx t m, HasVtyInput t m)
683− => Behavior t BoxStyle
684− -> Behavior t Text
685− -> m a
686− -> m a
687−boxTitle boxStyle title child = do
688− dh <- displayHeight
689− dw <- displayWidth
690− let boxReg = Region 0 0 <$> dw <*> dh
691− innerReg = Region 1 1 <$> (subtract 2 <$> dw) <*> (subtract 2 <$> dh)
692− tellImages (boxImages <$> title <*> boxStyle <*> current boxReg)
693− tellImages (fmap (\r -> [regionBlankImage r]) (current innerReg))
694− pane innerReg (pure True) child
695− where
696− boxImages :: Text -> BoxStyle -> Region -> [Image]
697− boxImages title' style (Region left top width height) =
698− let right = left + width - 1
699− bottom = top + height - 1
700− sides =
701− [ withinImage (Region (left + 1) top (width - 2) 1) $
702− V.text' V.defAttr $
703− hPadText title' (_boxStyle_n style) (width - 2)
704− , withinImage (Region right (top + 1) 1 (height - 2)) $
705− V.charFill V.defAttr (_boxStyle_e style) 1 (height - 2)
706− , withinImage (Region (left + 1) bottom (width - 2) 1) $
707− V.charFill V.defAttr (_boxStyle_s style) (width - 2) 1
708− , withinImage (Region left (top + 1) 1 (height - 2)) $
709− V.charFill V.defAttr (_boxStyle_w style) 1 (height - 2)
710− ]
711− corners =
712− [ withinImage (Region left top 1 1) $
713− V.char V.defAttr (_boxStyle_nw style)
714− , withinImage (Region right top 1 1) $
715− V.char V.defAttr (_boxStyle_ne style)
716− , withinImage (Region right bottom 1 1) $
717− V.char V.defAttr (_boxStyle_se style)
718− , withinImage (Region left bottom 1 1) $
719− V.char V.defAttr (_boxStyle_sw style)
720− ]
721− in sides ++ if width > 1 && height > 1 then corners else []
722− hPadText :: T.Text -> Char -> Int -> T.Text
723− hPadText t c l = if lt >= l
724− then t
725− else left <> t <> right
726− where
727− lt = T.length t
728− delta = l - lt
729− mkHalf n = T.replicate (n `div` 2) (T.singleton c)
730− left = mkHalf $ delta + 1
731− right = mkHalf delta
732−
733−-- | A box without a title
734−box :: (Monad m, Reflex t, MonadNodeId m, HasDisplayRegion t m, HasImageWriter t m, HasVtyWidgetCtx t m, HasVtyInput t m)
735− => Behavior t BoxStyle
736− -> m a
737− -> m a
738−box boxStyle = boxTitle boxStyle mempty
739−
740−-- | A box whose style is static
741−boxStatic
742− :: (Monad m, Reflex t, MonadNodeId m, HasDisplayRegion t m, HasImageWriter t m, HasVtyWidgetCtx t m, HasVtyInput t m)
743− => BoxStyle
744− -> m a
745− -> m a
746−boxStatic = box . pure
747−
748−-- | Configuration options for displaying "rich" text
749−data RichTextConfig t = RichTextConfig
750− { _richTextConfig_attributes :: Behavior t V.Attr
751− }
752−
753−instance Reflex t => Default (RichTextConfig t) where
754− def = RichTextConfig $ pure V.defAttr
755−
756−-- | A widget that displays text with custom time-varying attributes
757−richText
758− :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m)
759− => RichTextConfig t
760− -> Behavior t Text
761− -> m ()
762−richText cfg t = do
763− dw <- displayWidth
764− let img = (\w a s -> [wrapText w a s])
765− <$> current dw
766− <*> _richTextConfig_attributes cfg
767− <*> t
768− tellImages img
769− where
770− wrapText maxWidth attrs = V.vertCat
771− . concatMap (fmap (V.string attrs . T.unpack) . TZ.wrapWithOffset maxWidth 0)
772− . T.split (=='\n')
773−
774−-- | Renders text, wrapped to the container width
775−text
776− :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m)
777− => Behavior t Text
778− -> m ()
779−text = richText def
780−
781−-- | Scrollable text widget. The output pair exposes the current scroll position and total number of lines (including those
782−-- that are hidden)
783−scrollableText
784− :: forall t m. (Reflex t, MonadHold t m, MonadFix m, HasDisplayRegion t m, HasVtyInput t m, HasImageWriter t m)
785− => Event t Int
786− -- ^ Number of lines to scroll by
787− -> Behavior t Text
788− -> m (Behavior t (Int, Int))
789− -- ^ (Current scroll position, total number of lines)
790−scrollableText scrollBy t = do
791− dw <- displayWidth
792− let imgs = wrap <$> current dw <*> t
793− kup <- key V.KUp
794− kdown <- key V.KDown
795− m <- mouseScroll
796− let requestedScroll :: Event t Int
797− requestedScroll = leftmost
798− [ 1 <$ kdown
799− , (-1) <$ kup
800− , ffor m $ \case
801− ScrollDirection_Up -> (-1)
802− ScrollDirection_Down -> 1
803− , scrollBy
804− ]
805− updateLine maxN delta ix = min (max 0 (ix + delta)) maxN
806− lineIndex :: Dynamic t Int <- foldDyn (\(maxN, delta) ix -> updateLine (maxN - 1) delta ix) 0 $
807− attach (length <$> imgs) requestedScroll
808− tellImages $ fmap ((:[]) . V.vertCat) $ drop <$> current lineIndex <*> imgs
809− return $ (,) <$> ((+) <$> current lineIndex <*> pure 1) <*> (length <$> imgs)
810− where
811− wrap maxWidth = concatMap (fmap (V.string V.defAttr . T.unpack) . TZ.wrapWithOffset maxWidth 0) . T.split (=='\n')
812−
813−-- | Renders any behavior whose value can be converted to
814−-- 'String' as text
815−display
816− :: (Reflex t, Monad m, Show a, HasDisplayRegion t m, HasImageWriter t m)
817− => Behavior t a
818− -> m ()
819−display a = text $ T.pack . show <$> a
471+pane dr foc child = localRegion (const dr) $
472+ mapImages (imagesInRegion $ current dr) $
473+ localFocus (const foc) $
474+ inputInFocusedRegion >>= \e -> localInput (const e) child
820475
821476-- | A widget that draws nothing
822477blank :: Monad m => m ()
src/Reflex/Vty/Widget/Box.hsnew file+122
@@ -0,0 +1,122 @@
1+{-|
2+ Description: Drawing boxes in various styles
3+-}
4+module Reflex.Vty.Widget.Box where
5+
6+import Control.Monad.NodeId
7+import Data.Default
8+import Data.Text (Text)
9+import qualified Data.Text as T
10+import Graphics.Vty (Image)
11+import qualified Graphics.Vty as V
12+import Reflex
13+import Reflex.Vty.Widget
14+import Reflex.Vty.Widget.Text
15+
16+-- | Fill the background with the bottom box style
17+hRule :: (HasDisplayRegion t m, HasImageWriter t m) => BoxStyle -> m ()
18+hRule boxStyle = fill $ pure (_boxStyle_s boxStyle)
19+
20+-- | Defines a set of symbols to use to draw the outlines of boxes
21+-- C.f. https://en.wikipedia.org/wiki/Box-drawing_character
22+data BoxStyle = BoxStyle
23+ { _boxStyle_nw :: Char
24+ , _boxStyle_n :: Char
25+ , _boxStyle_ne :: Char
26+ , _boxStyle_e :: Char
27+ , _boxStyle_se :: Char
28+ , _boxStyle_s :: Char
29+ , _boxStyle_sw :: Char
30+ , _boxStyle_w :: Char
31+ }
32+
33+instance Default BoxStyle where
34+ def = singleBoxStyle
35+
36+-- | A box style that uses hyphens and pipe characters. Doesn't handle
37+-- corners very well.
38+hyphenBoxStyle :: BoxStyle
39+hyphenBoxStyle = BoxStyle '-' '-' '-' '|' '-' '-' '-' '|'
40+
41+-- | A single line box style
42+singleBoxStyle :: BoxStyle
43+singleBoxStyle = BoxStyle '┌' '─' '┐' '│' '┘' '─' '└' '│'
44+
45+-- | A thick single line box style
46+thickBoxStyle :: BoxStyle
47+thickBoxStyle = BoxStyle '┏' '━' '┓' '┃' '┛' '━' '┗' '┃'
48+
49+-- | A double line box style
50+doubleBoxStyle :: BoxStyle
51+doubleBoxStyle = BoxStyle '╔' '═' '╗' '║' '╝' '═' '╚' '║'
52+
53+-- | A single line box style with rounded corners
54+roundedBoxStyle :: BoxStyle
55+roundedBoxStyle = BoxStyle '╭' '─' '╮' '│' '╯' '─' '╰' '│'
56+
57+-- | Draws a titled box in the provided style and a child widget inside of that box
58+boxTitle :: (Monad m, Reflex t, MonadNodeId m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m)
59+ => Behavior t BoxStyle
60+ -> Behavior t Text
61+ -> m a
62+ -> m a
63+boxTitle boxStyle title child = do
64+ dh <- displayHeight
65+ dw <- displayWidth
66+ let boxReg = Region 0 0 <$> dw <*> dh
67+ innerReg = Region 1 1 <$> (subtract 2 <$> dw) <*> (subtract 2 <$> dh)
68+ tellImages (boxImages <$> title <*> boxStyle <*> current boxReg)
69+ tellImages (fmap (\r -> [regionBlankImage r]) (current innerReg))
70+ pane innerReg (pure True) child
71+ where
72+ boxImages :: Text -> BoxStyle -> Region -> [Image]
73+ boxImages title' style (Region left top width height) =
74+ let right = left + width - 1
75+ bottom = top + height - 1
76+ sides =
77+ [ withinImage (Region (left + 1) top (width - 2) 1) $
78+ V.text' V.defAttr $
79+ hPadText title' (_boxStyle_n style) (width - 2)
80+ , withinImage (Region right (top + 1) 1 (height - 2)) $
81+ V.charFill V.defAttr (_boxStyle_e style) 1 (height - 2)
82+ , withinImage (Region (left + 1) bottom (width - 2) 1) $
83+ V.charFill V.defAttr (_boxStyle_s style) (width - 2) 1
84+ , withinImage (Region left (top + 1) 1 (height - 2)) $
85+ V.charFill V.defAttr (_boxStyle_w style) 1 (height - 2)
86+ ]
87+ corners =
88+ [ withinImage (Region left top 1 1) $
89+ V.char V.defAttr (_boxStyle_nw style)
90+ , withinImage (Region right top 1 1) $
91+ V.char V.defAttr (_boxStyle_ne style)
92+ , withinImage (Region right bottom 1 1) $
93+ V.char V.defAttr (_boxStyle_se style)
94+ , withinImage (Region left bottom 1 1) $
95+ V.char V.defAttr (_boxStyle_sw style)
96+ ]
97+ in sides ++ if width > 1 && height > 1 then corners else []
98+ hPadText :: T.Text -> Char -> Int -> T.Text
99+ hPadText t c l = if lt >= l
100+ then t
101+ else left <> t <> right
102+ where
103+ lt = T.length t
104+ delta = l - lt
105+ mkHalf n = T.replicate (n `div` 2) (T.singleton c)
106+ left = mkHalf $ delta + 1
107+ right = mkHalf delta
108+
109+-- | A box without a title
110+box :: (Monad m, Reflex t, MonadNodeId m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m)
111+ => Behavior t BoxStyle
112+ -> m a
113+ -> m a
114+box boxStyle = boxTitle boxStyle mempty
115+
116+-- | A box whose style is static
117+boxStatic
118+ :: (Monad m, Reflex t, MonadNodeId m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m)
119+ => BoxStyle
120+ -> m a
121+ -> m a
122+boxStatic = box . pure
src/Reflex/Vty/Widget/Input.hs+15−6
@@ -8,6 +8,7 @@ module Reflex.Vty.Widget.Input
88 ) where
99
1010
11+import Reflex.Vty.Widget.Input.Mouse as Export
1112import Reflex.Vty.Widget.Input.Text as Export
1213
1314import Control.Monad (join)
@@ -18,6 +19,10 @@ import Data.Text (Text)
1819import qualified Graphics.Vty as V
1920import Reflex
2021import Reflex.Vty.Widget
22+import Reflex.Vty.Widget.Box
23+import Reflex.Vty.Widget.Text
24+
25+-- * Buttons
2126
2227-- | Configuration options for the 'button' widget
2328data ButtonConfig t = ButtonConfig
@@ -30,7 +35,7 @@ instance Reflex t => Default (ButtonConfig t) where
3035
3136-- | A button widget that contains a sub-widget
3237button
33− :: (Reflex t, Monad m, MonadNodeId m, HasFocus t m, HasDisplayRegion t m, HasImageWriter t m, HasVtyWidgetCtx t m, HasVtyInput t m)
38+ :: (Reflex t, Monad m, MonadNodeId m, HasFocusReader t m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m)
3439 => ButtonConfig t
3540 -> m ()
3641 -> m (Event t ())
@@ -48,7 +53,7 @@ button cfg child = do
4853
4954-- | A button widget that displays text that can change
5055textButton
51− :: (Reflex t, Monad m, MonadNodeId m, HasDisplayRegion t m, HasFocus t m, HasImageWriter t m, HasVtyWidgetCtx t m, HasVtyInput t m)
56+ :: (Reflex t, Monad m, MonadNodeId m, HasDisplayRegion t m, HasFocusReader t m, HasImageWriter t m, HasInput t m)
5257 => ButtonConfig t
5358 -> Behavior t Text
5459 -> m (Event t ())
@@ -56,15 +61,17 @@ textButton cfg = button cfg . text -- TODO Centering etc.
5661
5762-- | A button widget that displays a static bit of text
5863textButtonStatic
59− :: (Reflex t, Monad m, MonadNodeId m, HasDisplayRegion t m, HasFocus t m, HasImageWriter t m, HasVtyWidgetCtx t m, HasVtyInput t m)
64+ :: (Reflex t, Monad m, MonadNodeId m, HasDisplayRegion t m, HasFocusReader t m, HasImageWriter t m, HasInput t m)
6065 => ButtonConfig t
6166 -> Text
6267 -> m (Event t ())
6368textButtonStatic cfg = textButton cfg . pure
6469
70+-- * Links
71+
6572-- | A clickable link widget
6673link
67− :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m, HasVtyInput t m)
74+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m)
6875 => Behavior t Text
6976 -> m (Event t MouseUp)
7077link t = do
@@ -76,11 +83,13 @@ link t = do
7683
7784-- | A clickable link widget with a static label
7885linkStatic
79− :: (Reflex t, Monad m, HasImageWriter t m, HasDisplayRegion t m, HasVtyInput t m)
86+ :: (Reflex t, Monad m, HasImageWriter t m, HasDisplayRegion t m, HasInput t m)
8087 => Text
8188 -> m (Event t MouseUp)
8289linkStatic = link . pure
8390
91+-- * Checkboxes
92+
8493-- | Characters used to render checked and unchecked textboxes
8594data CheckboxStyle = CheckboxStyle
8695 { _checkboxStyle_unchecked :: Text
@@ -120,7 +129,7 @@ instance (Reflex t) => Default (CheckboxConfig t) where
120129
121130-- | A checkbox widget
122131checkbox
123− :: (MonadHold t m, MonadFix m, Reflex t, HasVtyInput t m, HasDisplayRegion t m, HasImageWriter t m, HasFocus t m)
132+ :: (MonadHold t m, MonadFix m, Reflex t, HasInput t m, HasDisplayRegion t m, HasImageWriter t m, HasFocusReader t m)
124133 => CheckboxConfig t
125134 -> Bool
126135 -> m (Dynamic t Bool)
src/Reflex/Vty/Widget/Input/Mouse.hsnew file+106
@@ -0,0 +1,106 @@
1+{-|
2+ Description: Mouse clicks, drags, and scrolls
3+-}
4+module Reflex.Vty.Widget.Input.Mouse where
5+
6+import Control.Monad.Fix
7+import qualified Graphics.Vty as V
8+import Reflex
9+import Reflex.Vty.Widget
10+
11+-- | Information about a drag operation
12+data Drag = Drag
13+ { _drag_from :: (Int, Int) -- ^ Where the drag began
14+ , _drag_to :: (Int, Int) -- ^ Where the mouse currently is
15+ , _drag_button :: V.Button -- ^ Which mouse button is dragging
16+ , _drag_modifiers :: [V.Modifier] -- ^ What modifiers are held
17+ , _drag_end :: Bool -- ^ Whether the drag ended (the mouse button was released)
18+ }
19+ deriving (Eq, Ord, Show)
20+
21+-- | Converts raw vty mouse drag events into an event stream of 'Drag's
22+drag
23+ :: (Reflex t, MonadFix m, MonadHold t m, HasInput t m)
24+ => V.Button
25+ -> m (Event t Drag)
26+drag btn = do
27+ inp <- input
28+ let f :: Maybe Drag -> V.Event -> Maybe Drag
29+ f Nothing = \case
30+ V.EvMouseDown x y btn' mods
31+ | btn == btn' -> Just $ Drag (x,y) (x,y) btn' mods False
32+ | otherwise -> Nothing
33+ _ -> Nothing
34+ f (Just (Drag from _ _ mods end)) = \case
35+ V.EvMouseDown x y btn' mods'
36+ | end && btn == btn' -> Just $ Drag (x,y) (x,y) btn' mods' False
37+ | btn == btn' -> Just $ Drag from (x,y) btn mods' False
38+ | otherwise -> Nothing -- Ignore other buttons.
39+ V.EvMouseUp x y (Just btn')
40+ | end -> Nothing
41+ | btn == btn' -> Just $ Drag from (x,y) btn mods True
42+ | otherwise -> Nothing
43+ V.EvMouseUp x y Nothing -- Terminal doesn't specify mouse up button,
44+ -- assume it's the right one.
45+ | end -> Nothing
46+ | otherwise -> Just $ Drag from (x,y) btn mods True
47+ _ -> Nothing
48+ rec let newDrag = attachWithMaybe f (current dragD) inp
49+ dragD <- holdDyn Nothing $ Just <$> newDrag
50+ return (fmapMaybe id $ updated dragD)
51+
52+-- | Mouse down events for a particular mouse button
53+mouseDown
54+ :: (Reflex t, Monad m, HasInput t m)
55+ => V.Button
56+ -> m (Event t MouseDown)
57+mouseDown btn = do
58+ i <- input
59+ return $ fforMaybe i $ \case
60+ V.EvMouseDown x y btn' mods -> if btn == btn'
61+ then Just $ MouseDown btn' (x, y) mods
62+ else Nothing
63+ _ -> Nothing
64+
65+-- | Mouse up events for a particular mouse button
66+mouseUp
67+ :: (Reflex t, Monad m, HasInput t m)
68+ => m (Event t MouseUp)
69+mouseUp = do
70+ i <- input
71+ return $ fforMaybe i $ \case
72+ V.EvMouseUp x y btn' -> Just $ MouseUp btn' (x, y)
73+ _ -> Nothing
74+
75+-- | Information about a mouse down event
76+data MouseDown = MouseDown
77+ { _mouseDown_button :: V.Button
78+ , _mouseDown_coordinates :: (Int, Int)
79+ , _mouseDown_modifiers :: [V.Modifier]
80+ }
81+ deriving (Eq, Ord, Show)
82+
83+-- | Information about a mouse up event
84+data MouseUp = MouseUp
85+ { _mouseUp_button :: Maybe V.Button
86+ , _mouseUp_coordinates :: (Int, Int)
87+ }
88+ deriving (Eq, Ord, Show)
89+
90+-- | Mouse scroll direction
91+data ScrollDirection = ScrollDirection_Up | ScrollDirection_Down
92+ deriving (Eq, Ord, Show)
93+
94+-- | Produce an event that fires when the mouse wheel is scrolled
95+mouseScroll
96+ :: (Reflex t, Monad m, HasInput t m)
97+ => m (Event t ScrollDirection)
98+mouseScroll = do
99+ up <- mouseDown V.BScrollUp
100+ down <- mouseDown V.BScrollDown
101+ return $ leftmost
102+ [ ScrollDirection_Up <$ up
103+ , ScrollDirection_Down <$ down
104+ ]
105+
106+
src/Reflex/Vty/Widget/Input/Text.hs+4−3
@@ -18,6 +18,7 @@ import Reflex
1818
1919import Reflex.Vty.Widget
2020import Reflex.Vty.Widget.Layout
21+import Reflex.Vty.Widget.Input.Mouse
2122
2223-- | Configuration options for a 'textInput'. For more information on
2324-- 'TextZipper', see 'Data.Text.Zipper'.
@@ -43,7 +44,7 @@ data TextInput t = TextInput
4344
4445-- | A widget that allows text input
4546textInput
46− :: (Reflex t, MonadHold t m, MonadFix m, HasVtyInput t m, HasFocus t m, HasDisplayRegion t m, HasImageWriter t m, HasDisplayRegion t m)
47+ :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m, HasFocusReader t m, HasDisplayRegion t m, HasImageWriter t m, HasDisplayRegion t m)
4748 => TextInputConfig t
4849 -> m (TextInput t)
4950textInput cfg = do
@@ -81,7 +82,7 @@ textInput cfg = do
8182
8283-- | A widget that allows multiline text input
8384multilineTextInput
84− :: (Reflex t, MonadHold t m, MonadFix m, HasVtyInput t m, HasFocus t m, HasDisplayRegion t m, HasImageWriter t m)
85+ :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m, HasFocusReader t m, HasDisplayRegion t m, HasImageWriter t m)
8586 => TextInputConfig t
8687 -> m (TextInput t)
8788multilineTextInput cfg = do
@@ -99,7 +100,7 @@ multilineTextInput cfg = do
99100-- the computed line count to greedily size the tile when vertically
100101-- oriented, and uses the fallback width when horizontally oriented.
101102textInputTile
102− :: (Monad m, MonadNodeId m, Reflex t, MonadFix m, MonadLayout t m, HasVtyWidgetCtx t m, HasVtyInput t m, MonadFocus t m, HasImageWriter t m, HasDisplayRegion t m)
103+ :: (Monad m, MonadNodeId m, Reflex t, MonadFix m, HasLayout t m, HasInput t m, HasFocus t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
103104 => m (TextInput t)
104105 -> Dynamic t Int
105106 -> m (TextInput t)
src/Reflex/Vty/Widget/Layout.hs+19−31
@@ -23,6 +23,7 @@ import qualified Graphics.Vty as V
2323import Reflex
2424import Reflex.Host.Class (MonadReflexCreateTrigger)
2525import Reflex.Vty.Widget
26+import Reflex.Vty.Widget.Input.Mouse
2627
2728-- * Focus
2829--
@@ -34,7 +35,7 @@ import Reflex.Vty.Widget
3435-- a tab press in a sibling element, a click event).
3536--
3637-- Focusable elements will usually be created via 'tile', but can also be
37−-- constructed via 'makeFocus' in 'MonadFocus'. The latter option allows for
38+-- constructed via 'makeFocus' in 'HasFocus'. The latter option allows for
3839-- more find-grained control of focus behavior.
3940
4041-- ** Storing focus state
@@ -75,7 +76,7 @@ shiftFS (FocusSet s) fid n = case OSet.findIndex <$> fid <*> pure s of
7576-- ** The focus management monad
7677
7778-- | A class for things that can produce focusable elements.
78−class (Monad m, Reflex t) => MonadFocus t m | m -> t where
79+class (Monad m, Reflex t) => HasFocus t m | m -> t where
7980 -- | Create a focusable element.
8081 makeFocus :: m FocusId
8182 -- | Emit an 'Event' of requests to change the focus.
@@ -124,18 +125,15 @@ instance (Adjustable t m, MonadHold t m, MonadFix m) => Adjustable t (Focus t m)
124125 traverseDMapWithKeyWithAdjust f m e = Focus $ traverseDMapWithKeyWithAdjust (\k v -> unFocus $ f k v) m e
125126 traverseDMapWithKeyWithAdjustWithMove f m e = Focus $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unFocus $ f k v) m e
126127
127−instance (Reflex t, MonadFix m, HasVtyInput t m) => HasVtyInput t (Focus t m) where
128+instance (Reflex t, MonadFix m, HasInput t m) => HasInput t (Focus t m) where
128129 localInput f = hoist (localInput f)
129130
130−instance (HasVtyWidgetCtx t m, Reflex t, MonadFix m) => HasVtyWidgetCtx t (Focus t m) where
131− localCtx f = hoist (localCtx f)
132−
133131instance (HasImageWriter t m, MonadFix m) => HasImageWriter t (Focus t m) where
134132 mapImages f = hoist (mapImages f)
135133
136−instance (HasFocus t m, Monad m) => HasFocus t (Focus t m)
134+instance (HasFocusReader t m, Monad m) => HasFocusReader t (Focus t m)
137135
138−instance (Reflex t, MonadFix m, MonadNodeId m) => MonadFocus t (Focus t m) where
136+instance (Reflex t, MonadFix m, MonadNodeId m) => HasFocus t (Focus t m) where
139137 makeFocus = do
140138 fid <- FocusId <$> lift getNextNodeId
141139 Focus $ tellDyn $ pure $ singletonFS fid
@@ -171,7 +169,7 @@ runFocus (Focus x) = do
171169-- | Runs an action in the focus monad, providing it with information about
172170-- whether any of the foci created within it are focused.
173171anyChildFocused
174− :: (MonadFocus t m, MonadFix m)
172+ :: (HasFocus t m, MonadFix m)
175173 => (Dynamic t Bool -> m a)
176174 -> m a
177175anyChildFocused f = do
@@ -186,7 +184,7 @@ anyChildFocused f = do
186184
187185-- | Request focus be shifted backward and forward based on tab presses. <Tab>
188186-- shifts focus forward and <Shift+Tab> shifts focus backward.
189−tabNavigation :: (Reflex t, MonadNodeId m, HasVtyInput t m, MonadFocus t m) => m ()
187+tabNavigation :: (Reflex t, MonadNodeId m, HasInput t m, HasFocus t m) => m ()
190188tabNavigation = do
191189 fwd <- fmap (const 1) <$> key (V.KChar '\t')
192190 back <- fmap (const (-1)) <$> key V.KBackTab
@@ -244,14 +242,14 @@ data Orientation = Orientation_Column
244242
245243-- | Create a row-oriented 'axis'
246244row
247− :: (Reflex t, MonadNodeId m, MonadFix m, MonadLayout t m)
245+ :: (Reflex t, MonadNodeId m, MonadFix m, HasLayout t m)
248246 => m a
249247 -> m a
250248row = axis (pure Orientation_Row) flex
251249
252250-- | Create a column-oriented 'axis'
253251col
254− :: (Reflex t, MonadNodeId m, MonadFix m, MonadLayout t m)
252+ :: (Reflex t, MonadNodeId m, MonadFix m, HasLayout t m)
255253 => m a
256254 -> m a
257255col = axis (pure Orientation_Column) flex
@@ -362,7 +360,7 @@ chunk o r (offset, sz) = case o of
362360-- ** The layout monad
363361
364362-- | A class of operations for creating screen layouts.
365−class Monad m => MonadLayout t m | m -> t where
363+class Monad m => HasLayout t m | m -> t where
366364 -- | Creates a parent element in the current layout with the given size
367365 -- constraint, which lays out its children according to the provided
368366 -- orientation.
@@ -408,16 +406,6 @@ instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (Layout t m
408406 traverseDMapWithKeyWithAdjust f m e = Layout $ traverseDMapWithKeyWithAdjust (\k v -> unLayout $ f k v) m e
409407 traverseDMapWithKeyWithAdjustWithMove f m e = Layout $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unLayout $ f k v) m e
410408
411−instance (HasVtyWidgetCtx t m, HasDisplayRegion t m, Reflex t, MonadFix m) => HasVtyWidgetCtx t (Layout t m) where
412− localCtx f x = do
413− solution <- Layout ask
414− let orientation = snd . rootLT <$> solution
415− lift $ localCtx f $ do
416− dw <- displayWidth
417− dh <- displayHeight
418− let reg = Region 0 0 <$> dw <*> dh
419− runLayout orientation reg x
420−
421409-- | Apply a transformation to the context of a child 'Layout' action and run
422410-- that action
423411hoistRunLayout
@@ -434,15 +422,15 @@ hoistRunLayout f x = do
434422 let reg = Region 0 0 <$> dw <*> dh
435423 runLayout orientation reg x
436424
437−instance (HasVtyInput t m, HasDisplayRegion t m, MonadFix m, Reflex t) => HasVtyInput t (Layout t m) where
425+instance (HasInput t m, HasDisplayRegion t m, MonadFix m, Reflex t) => HasInput t (Layout t m) where
438426 localInput = hoistRunLayout . localInput
439427
440428instance (HasDisplayRegion t m, HasImageWriter t m, MonadFix m) => HasImageWriter t (Layout t m) where
441429 mapImages f = hoistRunLayout (mapImages f)
442430
443−instance (HasFocus t m, Monad m) => HasFocus t (Layout t m)
431+instance (HasFocusReader t m, Monad m) => HasFocusReader t (Layout t m)
444432
445−instance (Monad m, MonadNodeId m, Reflex t, MonadFix m) => MonadLayout t (Layout t m) where
433+instance (Monad m, MonadNodeId m, Reflex t, MonadFix m) => HasLayout t (Layout t m) where
446434 axis o c (Layout x) = Layout $ do
447435 nodeId <- getNextNodeId
448436 (result, forest) <- lift $ local (\t -> fromMaybe (LayoutTree (Region 0 0 0 0, Orientation_Column) mempty) . lookupLF nodeId . childrenLT <$> t) $ runDynamicWriterT x
@@ -455,7 +443,7 @@ instance (Monad m, MonadNodeId m, Reflex t, MonadFix m) => MonadLayout t (Layout
455443 pure $ maybe (Region 0 0 0 0) (fst . rootLT) . lookupLF nodeId . childrenLT <$> solutions
456444 askOrientation = Layout $ asks $ fmap (snd . rootLT)
457445
458−instance (MonadFix m, MonadFocus t m) => MonadFocus t (Layout t m) where
446+instance (MonadFix m, HasFocus t m) => HasFocus t (Layout t m) where
459447 makeFocus = lift makeFocus
460448 requestFocus = lift . requestFocus
461449 isFocused = lift . isFocused
@@ -490,7 +478,7 @@ initLayout f = do
490478-- * The tile "window manager"
491479--
492480-- $tiling
493−-- Generally MonadLayout and MonadFocus are used together to build a user
481+-- Generally HasLayout and HasFocus are used together to build a user
494482-- interface. These functions check the available screen size and initialize
495483-- the layout monad with that information, and also initialize the focus monad.
496484
@@ -517,7 +505,7 @@ initManager_ = fmap fst . initManager
517505-- provided constraint. Returns the 'FocusId' allowing for manual focus
518506-- management.
519507tile'
520− :: (MonadNodeId m, MonadFix m, Reflex t, HasVtyWidgetCtx t m, HasVtyInput t m, MonadFocus t m, MonadLayout t m, HasImageWriter t m, HasDisplayRegion t m)
508+ :: (MonadNodeId m, MonadFix m, Reflex t, HasInput t m, HasFocus t m, HasLayout t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
521509 => Dynamic t Constraint
522510 -> m a
523511 -> m (FocusId, a)
@@ -536,7 +524,7 @@ tile' c w = do
536524-- | A widget that is focusable and occupies a layout region based on the
537525-- provided constraint.
538526tile
539− :: (MonadNodeId m, MonadFix m, Reflex t, HasVtyWidgetCtx t m, HasVtyInput t m, MonadFocus t m, MonadLayout t m, HasImageWriter t m, HasDisplayRegion t m)
527+ :: (MonadNodeId m, MonadFix m, Reflex t, HasInput t m, HasFocus t m, HasLayout t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
540528 => Dynamic t Constraint
541529 -> m a
542530 -> m a
@@ -547,7 +535,7 @@ tile c = fmap snd . tile' c
547535-- | A widget that is not focusable and occupies a layout region based on the
548536-- provided constraint.
549537grout
550− :: (Reflex t, MonadNodeId m, HasVtyWidgetCtx t m, MonadLayout t m, HasVtyInput t m, HasImageWriter t m, HasDisplayRegion t m)
538+ :: (Reflex t, MonadNodeId m, HasLayout t m, HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
551539 => Dynamic t Constraint
552540 -> m a
553541 -> m a
src/Reflex/Vty/Widget/Split.hsnew file+93
@@ -0,0 +1,93 @@
1+{-|
2+ Description: Widgets that split the display vertically or horizontally.
3+-}
4+module Reflex.Vty.Widget.Split where
5+
6+import Control.Applicative
7+import Control.Monad.Fix
8+import Control.Monad.NodeId
9+import Graphics.Vty as V
10+import Reflex
11+import Reflex.Vty.Widget
12+import Reflex.Vty.Widget.Input.Mouse
13+
14+-- | A split of the available space into two parts with a draggable separator.
15+-- Starts with half the space allocated to each, and the first pane has focus.
16+-- Clicking in a pane switches focus.
17+splitVDrag :: (Reflex t, MonadFix m, MonadHold t m, MonadNodeId m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m)
18+ => m ()
19+ -> m a
20+ -> m b
21+ -> m (a,b)
22+splitVDrag wS wA wB = do
23+ dh <- displayHeight
24+ dw <- displayWidth
25+ h0 <- sample $ current dh -- TODO
26+ dragE <- drag V.BLeft
27+ let splitter0 = h0 `div` 2
28+ rec splitterCheckpoint <- holdDyn splitter0 $ leftmost [fst <$> ffilter snd dragSplitter, resizeSplitter]
29+ splitterPos <- holdDyn splitter0 $ leftmost [fst <$> dragSplitter, resizeSplitter]
30+ splitterFrac <- holdDyn ((1::Double) / 2) $ ffor (attach (current dh) (fst <$> dragSplitter)) $ \(h, x) ->
31+ fromIntegral x / max 1 (fromIntegral h)
32+ let dragSplitter = fforMaybe (attach (current splitterCheckpoint) dragE) $
33+ \(splitterY, Drag (_, fromY) (_, toY) _ _ end) ->
34+ if splitterY == fromY then Just (toY, end) else Nothing
35+ regA = Region 0 0 <$> dw <*> splitterPos
36+ regS = Region 0 <$> splitterPos <*> dw <*> 1
37+ regB = Region 0 <$> (splitterPos + 1) <*> dw <*> (dh - splitterPos - 1)
38+ resizeSplitter = ffor (attach (current splitterFrac) (updated dh)) $
39+ \(frac, h) -> round (frac * fromIntegral h)
40+ focA <- holdDyn True $ leftmost
41+ [ True <$ mA
42+ , False <$ mB
43+ ]
44+ (mA, rA) <- pane regA focA $ withMouseDown wA
45+ pane regS (pure False) wS
46+ (mB, rB) <- pane regB (not <$> focA) $ withMouseDown wB
47+ return (rA, rB)
48+ where
49+ withMouseDown x = do
50+ m <- mouseDown V.BLeft
51+ x' <- x
52+ return (m, x')
53+
54+-- | A plain split of the available space into vertically stacked panes.
55+-- No visual separator is built in here.
56+splitV :: (Reflex t, Monad m, MonadNodeId m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m)
57+ => Dynamic t (Int -> Int)
58+ -- ^ Function used to determine size of first pane based on available size
59+ -> Dynamic t (Bool, Bool)
60+ -- ^ How to focus the two sub-panes, given that we are focused.
61+ -> m a
62+ -- ^ Widget for first pane
63+ -> m b
64+ -- ^ Widget for second pane
65+ -> m (a,b)
66+splitV sizeFunD focD wA wB = do
67+ dw <- displayWidth
68+ dh <- displayHeight
69+ let regA = Region 0 0 <$> dw <*> (sizeFunD <*> dh)
70+ regB = Region 0 <$> (_region_height <$> regA) <*> dw <*> liftA2 (-) dh (_region_height <$> regA)
71+ ra <- pane regA (fst <$> focD) wA
72+ rb <- pane regB (snd <$> focD) wB
73+ return (ra,rb)
74+
75+-- | A plain split of the available space into horizontally stacked panes.
76+-- No visual separator is built in here.
77+splitH :: (Reflex t, Monad m, MonadNodeId m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m)
78+ => Dynamic t (Int -> Int)
79+ -- ^ Function used to determine size of first pane based on available size
80+ -> Dynamic t (Bool, Bool)
81+ -- ^ How to focus the two sub-panes, given that we are focused.
82+ -> m a
83+ -- ^ Widget for first pane
84+ -> m b
85+ -- ^ Widget for second pane
86+ -> m (a,b)
87+splitH sizeFunD focD wA wB = do
88+ dw <- displayWidth
89+ dh <- displayHeight
90+ let regA = Region 0 0 <$> (sizeFunD <*> dw) <*> dh
91+ regB = Region <$> (_region_width <$> regA) <*> 0 <*> liftA2 (-) dw (_region_width <$> regA) <*> dh
92+ liftA2 (,) (pane regA (fmap fst focD) wA) (pane regB (fmap snd focD) wB)
93+
src/Reflex/Vty/Widget/Text.hsnew file+99
@@ -0,0 +1,99 @@
1+{-|
2+ Description: Text- and character-rendering widgets
3+-}
4+module Reflex.Vty.Widget.Text where
5+
6+import Control.Monad.Fix
7+import Data.Default
8+import Data.Text (Text)
9+import qualified Data.Text as T
10+import Data.Text.Zipper as TZ
11+import qualified Graphics.Vty as V
12+import Reflex
13+import Reflex.Vty.Widget
14+import Reflex.Vty.Widget.Input.Mouse
15+
16+-- | Fill the background with a particular character.
17+fill :: (HasDisplayRegion t m, HasImageWriter t m) => Behavior t Char -> m ()
18+fill bc = do
19+ dw <- displayWidth
20+ dh <- displayHeight
21+ let fillImg =
22+ (\w h c -> [V.charFill V.defAttr c w h])
23+ <$> current dw
24+ <*> current dh
25+ <*> bc
26+ tellImages fillImg
27+
28+-- | Configuration options for displaying "rich" text
29+data RichTextConfig t = RichTextConfig
30+ { _richTextConfig_attributes :: Behavior t V.Attr
31+ }
32+
33+instance Reflex t => Default (RichTextConfig t) where
34+ def = RichTextConfig $ pure V.defAttr
35+
36+-- | A widget that displays text with custom time-varying attributes
37+richText
38+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m)
39+ => RichTextConfig t
40+ -> Behavior t Text
41+ -> m ()
42+richText cfg t = do
43+ dw <- displayWidth
44+ let img = (\w a s -> [wrapText w a s])
45+ <$> current dw
46+ <*> _richTextConfig_attributes cfg
47+ <*> t
48+ tellImages img
49+ where
50+ wrapText maxWidth attrs = V.vertCat
51+ . concatMap (fmap (V.string attrs . T.unpack) . TZ.wrapWithOffset maxWidth 0)
52+ . T.split (=='\n')
53+
54+-- | Renders text, wrapped to the container width
55+text
56+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m)
57+ => Behavior t Text
58+ -> m ()
59+text = richText def
60+
61+-- | Scrollable text widget. The output pair exposes the current scroll position and total number of lines (including those
62+-- that are hidden)
63+scrollableText
64+ :: forall t m. (Reflex t, MonadHold t m, MonadFix m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m)
65+ => Event t Int
66+ -- ^ Number of lines to scroll by
67+ -> Behavior t Text
68+ -> m (Behavior t (Int, Int))
69+ -- ^ (Current scroll position, total number of lines)
70+scrollableText scrollBy t = do
71+ dw <- displayWidth
72+ let imgs = wrap <$> current dw <*> t
73+ kup <- key V.KUp
74+ kdown <- key V.KDown
75+ m <- mouseScroll
76+ let requestedScroll :: Event t Int
77+ requestedScroll = leftmost
78+ [ 1 <$ kdown
79+ , (-1) <$ kup
80+ , ffor m $ \case
81+ ScrollDirection_Up -> (-1)
82+ ScrollDirection_Down -> 1
83+ , scrollBy
84+ ]
85+ updateLine maxN delta ix = min (max 0 (ix + delta)) maxN
86+ lineIndex :: Dynamic t Int <- foldDyn (\(maxN, delta) ix -> updateLine (maxN - 1) delta ix) 0 $
87+ attach (length <$> imgs) requestedScroll
88+ tellImages $ fmap ((:[]) . V.vertCat) $ drop <$> current lineIndex <*> imgs
89+ return $ (,) <$> ((+) <$> current lineIndex <*> pure 1) <*> (length <$> imgs)
90+ where
91+ wrap maxWidth = concatMap (fmap (V.string V.defAttr . T.unpack) . TZ.wrapWithOffset maxWidth 0) . T.split (=='\n')
92+
93+-- | Renders any behavior whose value can be converted to
94+-- 'String' as text
95+display
96+ :: (Reflex t, Monad m, Show a, HasDisplayRegion t m, HasImageWriter t m)
97+ => Behavior t a
98+ -> m ()
99+display a = text $ T.pack . show <$> a