on develop help sign in
Get rid of VtyWidget and VtyWidgetCtx. Move functions to more appropriate modules; Consistent naming
e3df5c0Ali Abrar
ChangeLog.md+75−57
@@ -1,63 +1,81 @@
1# Revision history for reflex-vty
1# Revision history for reflex-vty
2
2
3## 0.2.0.0
3## 0.2.0.0
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_:
4−* Remove text-icu dependency and switch to `wcwidth` from vty package to compute character width in `Data.Text.Zipper`.
11+ * 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.
12+ * `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).
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_:
7−* _Breaking Change_: Layout and focus have been substantially refactored:
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.
8− * Added `MonadFocus` to produce focusable elements, and manage focus state. See the "Focus" section of the Reflex.Vty.Widget.Layout module documentation.
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.
9− * `Layout` no longer has any focus-tracking responsibility. See the "Layout" 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.
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.
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.
11− * Calls to `fixed` and `stretch` must now be replaced with `tile . fixed` and `tile . stretch`
20+ * Calls to `fixed` and `stretch` must now be replaced with `tile . fixed` and `tile . stretch`
12− * `stretch` now takes a minimum size argument
21+ * `stretch` now takes a minimum size argument
13− * Added `flex` which is equivalent to `stretch 0`
22+ * 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.
23+ * `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`.
24+ * Added `axis` (in `HasLayout`), 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`
25+ * Added `region` (in `HasLayout`), 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)
26+ * 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
27+ * Removed `VtyWidget` and replaced it with a number of separate classes and monad transformers
19−* Add default instances for `HasVtyInput`, `HasFocus`, and `ImageWriter`
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
20−* _Breaking Change_: Remove `DynRegion` and `currentRegion`. Use `Dynamic t Region` and `current` instead. This also changes the type of `pane`'s argument.
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`
21−* _Breaking Change_: The following functions are no longer specialized to `VtyWidget`:
43+ * The following functions are no longer specialized to `VtyWidget`:
22− * `pane`: Now requires `HasVtyWidgetCtx` instead
44+ * `pane`: Now requires `HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m`
23− * `drag`: Now requires `HasVtyInput` instead
45+ * `drag`: Now requires `HasInput`
24− * `mouseDown`: Now requires `HasVtyInput` instead
46+ * `mouseDown`: Now requires `HasInput`
25− * `mouseUp`: Now requires `HasVtyInput` instead
47+ * `mouseUp`: Now requires `HasInput`
26− * `mouseScroll`: Now requires `HasVtyInput` instead
48+ * `mouseScroll`: Now requires `HasInput`
27− * `key`: Now requires `HasVtyInput` instead
49+ * `key`: Now requires `HasInput`
28− * `keys`: Now requires `HasVtyInput` instead
50+ * `keys`: Now requires `HasInput`
29− * `keyCombo`: Now requires `HasVtyInput` instead
51+ * `keyCombo`: Now requires `HasInput`
30− * `keyCombos`: Now requires `HasVtyInput` instead
52+ * `keyCombos`: Now requires `HasInput`
31− * `splitV`: Now requires `HasVtyWidgetCtx` and `HasDisplaySize` instead
53+ * `splitV`: Now requires `HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m`
32− * `splitH`: Now requires `HasVtyWidgetCtx` and `HasDisplaySize` instead
54+ * `splitH`: Now requires `HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m`
33− * `splitVDrag`: Now requires `HasVtyWidgetCtx`, `HasVtyInput`, and `HasDisplaySize` instead
55+ * `splitVDrag`: Now requires `HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m`
34− * `fill`: Now requires `ImageWriter` and `HasDisplaySize` instead
56+ * `fill`: Now requires `HasImageWriter` and `HasDisplayRegion`
35− * `boxTitle`: Now requires `HasVtyWidgetCtx`, `ImageWriter`, and `HasDisplaySize` instead
57+ * `boxTitle`: Now requires `HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m`
36− * `box`: Now requires `HasVtyWidgetCtx`, `ImageWriter`, and `HasDisplaySize` instead
58+ * `box`: Now requires `HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m`
37− * `boxStatic`: Now requires `HasVtyWidgetCtx`, `ImageWriter`, and `HasDisplaySize` instead
59+ * `boxStatic`: Now requires `HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m`
38− * `richText`: Now requires `ImageWriter`, and `HasDisplaySize` instead
60+ * `richText`: Now requires `HasImageWriter`, and `HasDisplayRegion`
39− * `scrollableText`: Now requires `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
61+ * `scrollableText`: Now requires `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
40− * `blank`: Now required `Monad` instead
62+ * `blank`: Now requires `Monad`
41− * `button`: Now requires `HasFocus`, `HasVtyWidgetCtx`, `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
63+ * `button`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
42− * `textButton`: Now requires `HasFocus`, `HasVtyWidgetCtx`, `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
64+ * `textButton`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
43− * `textButtonStatic`: Now requires `HasFocus`, `HasVtyWidgetCtx`, `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
65+ * `textButtonStatic`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
44− * `link`: Now requires `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
66+ * `link`: Now requires `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
45− * `checkbox`: Now requires `HasFocus`, `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
67+ * `checkbox`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
46− * `textInput`: Now requires `HasFocus`, `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
68+ * `textInput`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
47− * `multilineTextInput`: Now requires `HasFocus`, `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
69+ * `multilineTextInput`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
48− * `textInputTile`: Now requires `HasVtyWidgetCtx`, `HasVtyInput`, `MonadLayout`, and `MonadFocus` instead
70+ * `textInputTile`: Now requires `HasFocusReader`, `HasInput`, `HasLayout`, and `HasFocus`
49−* _Breaking Change_: `CheckboxConfig` now has a field taking an `Event` to set the value of the checkbox.
71+* _Misc_:
50−* _Breaking Change_: `checkbox` now accepts keyboard input (spacebar to check and uncheck) and is displayed in bold when focused.
72+ * Add default instances for `HasInput`, `HasFocus`, and `HasImageWriter`
51−* _Breaking Change_: `HasVtyInput` now has a method `localInput` for filtering the input a child widget may receive
73+ * Export `withinImage` and add `imagesInRegion` to crop images to a region
52−* Add `anyChildFocused`, which provides information about whether subwidgets are focused
74+ * 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
75+ * 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
76+ * Add `hoistRunLayout` to apply a transformation to the context of a `Layout` action and run that action
56−* Add an `MFunctor` instance for `VtyWidget`
77+ * Add various `MFunctor` instances
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
78+ * 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`
61
79
62## 0.1.4.1
80## 0.1.4.1
63* Migrate to new dependent-sum / dependent-map (after the "some" package split)
81* Migrate to new dependent-sum / dependent-map (after the "some" package split)
reflex-vty.cabal+4
@@ -23,9 +23,13 @@ library
23 exposed-modules: Reflex.Vty
23 exposed-modules: Reflex.Vty
24 , Reflex.Vty.Host
24 , Reflex.Vty.Host
25 , Reflex.Vty.Widget
25 , Reflex.Vty.Widget
26+ , Reflex.Vty.Widget.Box
26 , Reflex.Vty.Widget.Input
27 , Reflex.Vty.Widget.Input
28+ , Reflex.Vty.Widget.Input.Mouse
27 , Reflex.Vty.Widget.Input.Text
29 , Reflex.Vty.Widget.Input.Text
28 , Reflex.Vty.Widget.Layout
30 , Reflex.Vty.Widget.Layout
31+ , Reflex.Vty.Widget.Split
32+ , Reflex.Vty.Widget.Text
29 , Data.Text.Zipper
33 , Data.Text.Zipper
30 , Reflex.Class.Switchable
34 , Reflex.Class.Switchable
31 , Reflex.Spider.Orphans
35 , Reflex.Spider.Orphans
src/Reflex/Vty.hs+6
@@ -14,15 +14,21 @@ module Reflex.Vty
14 ( module Reflex
14 ( module Reflex
15 , module Reflex.Vty.Host
15 , module Reflex.Vty.Host
16 , module Reflex.Vty.Widget
16 , module Reflex.Vty.Widget
17+ , module Reflex.Vty.Widget.Box
17 , module Reflex.Vty.Widget.Input
18 , module Reflex.Vty.Widget.Input
18 , module Reflex.Vty.Widget.Layout
19 , module Reflex.Vty.Widget.Layout
20+ , module Reflex.Vty.Widget.Split
21+ , module Reflex.Vty.Widget.Text
19 , module Control.Monad.NodeId
22 , module Control.Monad.NodeId
20 ) where
23 ) where
21
24
22import Reflex
25import Reflex
23import Reflex.Vty.Host
26import Reflex.Vty.Host
24import Reflex.Vty.Widget
27import Reflex.Vty.Widget
28+import Reflex.Vty.Widget.Box
25import Reflex.Vty.Widget.Input
29import Reflex.Vty.Widget.Input
26import Reflex.Vty.Widget.Layout
30import Reflex.Vty.Widget.Layout
31+import Reflex.Vty.Widget.Split
32+import Reflex.Vty.Widget.Text
27
33
28import Control.Monad.NodeId
34import 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
5{-# Language UndecidableInstances #-}
5{-# Language UndecidableInstances #-}
6
6
7module Reflex.Vty.Widget where
7module 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
58
8
59import Control.Applicative (liftA2)
9import Control.Applicative (liftA2)
60import Control.Monad.Fix (MonadFix)
10import Control.Monad.Fix (MonadFix)
61import Control.Monad.IO.Class (MonadIO)
11import Control.Monad.IO.Class (MonadIO)
62import Control.Monad.Morph
12import Control.Monad.Morph
63import Control.Monad.NodeId
13import Control.Monad.NodeId
64−import Control.Monad.Reader (ReaderT, ask, asks, local, runReaderT)
14+import Control.Monad.Reader (ReaderT, ask, local, runReaderT)
65import Control.Monad.Ref
15import Control.Monad.Ref
66import Control.Monad.Trans (MonadTrans, lift)
16import Control.Monad.Trans (MonadTrans, lift)
67−import Data.Default (Default(..))
68import Data.Set (Set)
17import Data.Set (Set)
69import qualified Data.Set as Set
18import 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
73import Graphics.Vty (Image)
19import Graphics.Vty (Image)
74import qualified Graphics.Vty as V
20import qualified Graphics.Vty as V
75import Reflex
21import Reflex
@@ -77,80 +23,9 @@ import Reflex.Class ()
77import Reflex.Host.Class (MonadReflexCreateTrigger)
23import Reflex.Host.Class (MonadReflexCreateTrigger)
78import Reflex.Vty.Host
24import Reflex.Vty.Host
79
25
80−-- | The context within which a 'VtyWidget' runs
26+-- * Running a vty application
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− }
85
27
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
154mainWidgetWithHandle
29mainWidgetWithHandle
155 :: V.Vty
30 :: V.Vty
156 -> (forall t m.
31 -> (forall t m.
@@ -158,7 +33,9 @@ mainWidgetWithHandle
158 , HasImageWriter t m
33 , HasImageWriter t m
159 , MonadNodeId m
34 , MonadNodeId m
160 , HasDisplayRegion t m
35 , HasDisplayRegion t m
36+ , HasFocusReader t m
37+ , HasInput t m
161− ) => VtyWidget t m (Event t ()))
38+ ) => m (Event t ()))
162 -> IO ()
39 -> IO ()
163mainWidgetWithHandle vty child =
40mainWidgetWithHandle vty child =
164 runVtyAppWithHandle vty $ \dr0 inp -> do
41 runVtyAppWithHandle vty $ \dr0 inp -> do
@@ -168,13 +45,10 @@ mainWidgetWithHandle vty child =
168 let inp' = fforMaybe inp $ \case
45 let inp' = fforMaybe inp $ \case
169 V.EvResize {} -> Nothing
46 V.EvResize {} -> Nothing
170 x -> Just x
47 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) $
175 runImageWriter $
49 runImageWriter $
176 runNodeIdT $
50 runNodeIdT $
177− runVtyWidget inp' ctx $ do
51+ runInput inp' $ do
178 tellImages . ffor (current size) $ \(w, h) -> [V.charFill V.defAttr ' ' w h]
52 tellImages . ffor (current size) $ \(w, h) -> [V.charFill V.defAttr ' ' w h]
179 child
53 child
180 return $ VtyResult
54 return $ VtyResult
@@ -182,6 +56,11 @@ mainWidgetWithHandle vty child =
182 , _vtyResult_shutdown = shutdown
56 , _vtyResult_shutdown = shutdown
183 }
57 }
184
58
59+-- | The output of a vty widget
60+data VtyWidgetOut t = VtyWidgetOut
61+ { _vtyWidgetOut_shutdown :: Event t ()
62+ }
63+
185-- | Like 'mainWidgetWithHandle', but uses a default vty configuration
64-- | Like 'mainWidgetWithHandle', but uses a default vty configuration
186mainWidget
65mainWidget
187 :: (forall t m.
66 :: (forall t m.
@@ -189,12 +68,167 @@ mainWidget
189 , HasImageWriter t m
68 , HasImageWriter t m
190 , MonadNodeId m
69 , MonadNodeId m
191 , HasDisplayRegion t m
70 , HasDisplayRegion t m
71+ , HasFocusReader t m
72+ , HasInput t m
192− ) => VtyWidget t m (Event t ()))
73+ ) => m (Event t ()))
193 -> IO ()
74 -> IO ()
194mainWidget child = do
75mainWidget child = do
195 vty <- getDefaultVty
76 vty <- getDefaultVty
196 mainWidgetWithHandle vty child
77 mainWidgetWithHandle vty child
197
78
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+
198-- | A class for things that know their own display size dimensions
232-- | A class for things that know their own display size dimensions
199class (Reflex t, Monad m) => HasDisplayRegion t m | m -> t where
233class (Reflex t, Monad m) => HasDisplayRegion t m | m -> t where
200 -- | Retrieve the display region
234 -- | Retrieve the display region
@@ -206,9 +240,11 @@ class (Reflex t, Monad m) => HasDisplayRegion t m | m -> t where
206 default localRegion :: (f m' ~ m, Monad m', MFunctor f, HasDisplayRegion t m') => (Dynamic t Region -> Dynamic t Region) -> m a -> m a
240 default localRegion :: (f m' ~ m, Monad m', MFunctor f, HasDisplayRegion t m') => (Dynamic t Region -> Dynamic t Region) -> m a -> m a
207 localRegion f = hoist (localRegion f)
241 localRegion f = hoist (localRegion f)
208
242
243+-- | Retrieve the display width
209displayWidth :: HasDisplayRegion t m => m (Dynamic t Int)
244displayWidth :: HasDisplayRegion t m => m (Dynamic t Int)
210displayWidth = fmap _region_width <$> askRegion
245displayWidth = fmap _region_width <$> askRegion
211
246
247+-- | Retrieve the display height
212displayHeight :: HasDisplayRegion t m => m (Dynamic t Int)
248displayHeight :: HasDisplayRegion t m => m (Dynamic t Int)
213displayHeight = fmap _region_height <$> askRegion
249displayHeight = fmap _region_height <$> askRegion
214
250
@@ -218,7 +254,9 @@ instance HasDisplayRegion t m => HasDisplayRegion t (DynamicWriterT t x m)
218instance HasDisplayRegion t m => HasDisplayRegion t (EventWriterT t x m)
254instance HasDisplayRegion t m => HasDisplayRegion t (EventWriterT t x m)
219instance HasDisplayRegion t m => HasDisplayRegion t (NodeIdT m)
255instance HasDisplayRegion t m => HasDisplayRegion t (NodeIdT m)
220
256
257+-- | A widget that has access to a particular region of the vty display
258+newtype DisplayRegion t m a = DisplayRegion
221−newtype DisplayRegion t m a = DisplayRegion { unDisplayRegion :: ReaderT (Dynamic t Region) m a }
259+ { unDisplayRegion :: ReaderT (Dynamic t Region) m a }
222 deriving
260 deriving
223 ( Functor
261 ( Functor
224 , Applicative
262 , Applicative
@@ -240,6 +278,7 @@ deriving instance PerformEvent t m => PerformEvent t (DisplayRegion t m)
240deriving instance PostBuild t m => PostBuild t (DisplayRegion t m)
278deriving instance PostBuild t m => PostBuild t (DisplayRegion t m)
241deriving instance TriggerEvent t m => TriggerEvent t (DisplayRegion t m)
279deriving instance TriggerEvent t m => TriggerEvent t (DisplayRegion t m)
242instance HasImageWriter t m => HasImageWriter t (DisplayRegion t m)
280instance HasImageWriter t m => HasImageWriter t (DisplayRegion t m)
281+instance HasFocusReader t m => HasFocusReader t (DisplayRegion t m)
243
282
244instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (DisplayRegion t m) where
283instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (DisplayRegion t m) where
245 runWithReplace (DisplayRegion a) e = DisplayRegion $ runWithReplace a $ fmap unDisplayRegion e
284 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
247 traverseDMapWithKeyWithAdjust f m e = DisplayRegion $ traverseDMapWithKeyWithAdjust (\k v -> unDisplayRegion $ f k v) m e
286 traverseDMapWithKeyWithAdjust f m e = DisplayRegion $ traverseDMapWithKeyWithAdjust (\k v -> unDisplayRegion $ f k v) m e
248 traverseDMapWithKeyWithAdjustWithMove f m e = DisplayRegion $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unDisplayRegion $ f k v) m e
287 traverseDMapWithKeyWithAdjustWithMove f m e = DisplayRegion $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unDisplayRegion $ f k v) m e
249
288
250−
251instance MonadTrans (DisplayRegion t) where
289instance MonadTrans (DisplayRegion t) where
252 lift = DisplayRegion . lift
290 lift = DisplayRegion . lift
253
291
@@ -256,6 +294,7 @@ instance MFunctor (DisplayRegion t) where
256
294
257instance MonadNodeId m => MonadNodeId (DisplayRegion t m)
295instance MonadNodeId m => MonadNodeId (DisplayRegion t m)
258
296
297+-- | Run a 'DisplayRegion' action with a given 'Region'
259runDisplayRegion
298runDisplayRegion
260 :: (Reflex t, Monad m)
299 :: (Reflex t, Monad m)
261 => Dynamic t Region
300 => Dynamic t Region
@@ -263,33 +302,71 @@ runDisplayRegion
263 -> m a
302 -> m a
264runDisplayRegion r = flip runReaderT r . unDisplayRegion
303runDisplayRegion r = flip runReaderT r . unDisplayRegion
265
304
266−-- | A class for things that can receive vty events as input
305+-- * Getting focus state
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
284
306
285-- | A class for things that can dynamically gain and lose focus
307-- | 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
287 focus :: m (Dynamic t Bool)
309 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)
289 focus = lift focus
311 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
290
352
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
291−instance (Reflex t, Monad m) => HasFocus t (VtyWidget t m) where
363+ :: (Reflex t, Monad m)
292− focus = VtyWidget $ asks _vtyWidgetCtx_focus
364+ => Dynamic t Bool
365+ -> FocusReader t m a
366+ -> m a
367+runFocusReader b = flip runReaderT b . unFocusReader
368+
369+-- * "Image" output
293
370
294-- | A class for widgets that can produce images to draw to the display
371-- | A class for widgets that can produce images to draw to the display
295class (Reflex t, Monad m) => HasImageWriter t m | m -> t where
372class (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
302 default mapImages :: (f m' ~ m, Monad m', MFunctor f, HasImageWriter t m') => (Behavior t [Image] -> Behavior t [Image]) -> m a -> m a
379 default mapImages :: (f m' ~ m, Monad m', MFunctor f, HasImageWriter t m') => (Behavior t [Image] -> Behavior t [Image]) -> m a -> m a
303 mapImages f = hoist (mapImages f)
380 mapImages f = hoist (mapImages f)
304
381
382+-- | A widget that can produce images to draw onto the display
383+newtype ImageWriter t m a = ImageWriter
305−newtype ImageWriter t m a = ImageWriter { unImageWriter :: BehaviorWriterT t [Image] m a }
384+ { unImageWriter :: BehaviorWriterT t [Image] m a }
306 deriving
385 deriving
307 ( Functor
386 ( Functor
308 , Applicative
387 , Applicative
@@ -345,30 +424,16 @@ instance (Monad m, Reflex t) => HasImageWriter t (ImageWriter t m) where
345 pure a
424 pure a
346
425
347instance HasDisplayRegion t m => HasDisplayRegion t (ImageWriter t m)
426instance HasDisplayRegion t m => HasDisplayRegion t (ImageWriter t m)
427+instance HasFocusReader t m => HasFocusReader t (ImageWriter t m)
348
428
429+-- | Run a widget that can produce images
349runImageWriter
430runImageWriter
350 :: (Reflex t, Monad m)
431 :: (Reflex t, Monad m)
351 => ImageWriter t m a
432 => ImageWriter t m a
352 -> m (a, Behavior t [Image])
433 -> m (a, Behavior t [Image])
353runImageWriter = runBehaviorWriterT . unImageWriter
434runImageWriter = runBehaviorWriterT . unImageWriter
354
435
355−-- | A chunk of the display area
436+-- ** Manipulating images
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
372
437
373-- | Translates and crops an 'Image' so that it is contained by
438-- | Translates and crops an 'Image' so that it is contained by
374-- the given 'Region'.
439-- the given 'Region'.
@@ -380,7 +445,17 @@ withinImage (Region left top width height)
380 | width < 0 || height < 0 = withinImage (Region left top 0 0)
445 | width < 0 || height < 0 = withinImage (Region left top 0 0)
381 | otherwise = V.translate left top . V.crop width height
446 | otherwise = V.translate left top . V.crop width height
382
447
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+
383−-- | Low-level widget combinator that runs a child 'VtyWidget' within
458+-- | Low-level widget combinator that runs a child 'Input' within
384-- a given region and context. This widget filters and modifies the input
459-- a given region and context. This widget filters and modifies the input
385-- that the child widget receives such that:
460-- that the child widget receives such that:
386-- * unfocused widgets receive no key events
461-- * unfocused widgets receive no key events
@@ -388,435 +463,15 @@ withinImage (Region left top width height)
388-- * mouse inputs inside the region have their coordinates translated such
463-- * mouse inputs inside the region have their coordinates translated such
389-- that (0,0) is the top-left corner of the region
464-- that (0,0) is the top-left corner of the region
390pane
465pane
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)
392 => Dynamic t Region
467 => Dynamic t Region
393 -> Dynamic t Bool -- ^ Whether the widget should be focused when the parent is.
468 -> Dynamic t Bool -- ^ Whether the widget should be focused when the parent is.
394 -> m a
469 -> m a
395 -> m a
470 -> m a
396−pane dr foc child = do
471+pane dr foc child = localRegion (const dr) $
397− let reg = current dr
472+ mapImages (imagesInRegion $ current dr) $
398− let subContext ctx = VtyWidgetCtx
473+ localFocus (const foc) $
399− { _vtyWidgetCtx_focus = liftA2 (&&) (_vtyWidgetCtx_focus ctx) foc
474+ inputInFocusedRegion >>= \e -> localInput (const e) child
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
820
475
821-- | A widget that draws nothing
476-- | A widget that draws nothing
822blank :: Monad m => m ()
477blank :: 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
8 ) where
8 ) where
9
9
10
10
11+import Reflex.Vty.Widget.Input.Mouse as Export
11import Reflex.Vty.Widget.Input.Text as Export
12import Reflex.Vty.Widget.Input.Text as Export
12
13
13import Control.Monad (join)
14import Control.Monad (join)
@@ -18,6 +19,10 @@ import Data.Text (Text)
18import qualified Graphics.Vty as V
19import qualified Graphics.Vty as V
19import Reflex
20import Reflex
20import Reflex.Vty.Widget
21import Reflex.Vty.Widget
22+import Reflex.Vty.Widget.Box
23+import Reflex.Vty.Widget.Text
24+
25+-- * Buttons
21
26
22-- | Configuration options for the 'button' widget
27-- | Configuration options for the 'button' widget
23data ButtonConfig t = ButtonConfig
28data ButtonConfig t = ButtonConfig
@@ -30,7 +35,7 @@ instance Reflex t => Default (ButtonConfig t) where
30
35
31-- | A button widget that contains a sub-widget
36-- | A button widget that contains a sub-widget
32button
37button
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)
34 => ButtonConfig t
39 => ButtonConfig t
35 -> m ()
40 -> m ()
36 -> m (Event t ())
41 -> m (Event t ())
@@ -48,7 +53,7 @@ button cfg child = do
48
53
49-- | A button widget that displays text that can change
54-- | A button widget that displays text that can change
50textButton
55textButton
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)
52 => ButtonConfig t
57 => ButtonConfig t
53 -> Behavior t Text
58 -> Behavior t Text
54 -> m (Event t ())
59 -> m (Event t ())
@@ -56,15 +61,17 @@ textButton cfg = button cfg . text -- TODO Centering etc.
56
61
57-- | A button widget that displays a static bit of text
62-- | A button widget that displays a static bit of text
58textButtonStatic
63textButtonStatic
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)
60 => ButtonConfig t
65 => ButtonConfig t
61 -> Text
66 -> Text
62 -> m (Event t ())
67 -> m (Event t ())
63textButtonStatic cfg = textButton cfg . pure
68textButtonStatic cfg = textButton cfg . pure
64
69
70+-- * Links
71+
65-- | A clickable link widget
72-- | A clickable link widget
66link
73link
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)
68 => Behavior t Text
75 => Behavior t Text
69 -> m (Event t MouseUp)
76 -> m (Event t MouseUp)
70link t = do
77link t = do
@@ -76,11 +83,13 @@ link t = do
76
83
77-- | A clickable link widget with a static label
84-- | A clickable link widget with a static label
78linkStatic
85linkStatic
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)
80 => Text
87 => Text
81 -> m (Event t MouseUp)
88 -> m (Event t MouseUp)
82linkStatic = link . pure
89linkStatic = link . pure
83
90
91+-- * Checkboxes
92+
84-- | Characters used to render checked and unchecked textboxes
93-- | Characters used to render checked and unchecked textboxes
85data CheckboxStyle = CheckboxStyle
94data CheckboxStyle = CheckboxStyle
86 { _checkboxStyle_unchecked :: Text
95 { _checkboxStyle_unchecked :: Text
@@ -120,7 +129,7 @@ instance (Reflex t) => Default (CheckboxConfig t) where
120
129
121-- | A checkbox widget
130-- | A checkbox widget
122checkbox
131checkbox
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)
124 => CheckboxConfig t
133 => CheckboxConfig t
125 -> Bool
134 -> Bool
126 -> m (Dynamic t Bool)
135 -> 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
18
18
19import Reflex.Vty.Widget
19import Reflex.Vty.Widget
20import Reflex.Vty.Widget.Layout
20import Reflex.Vty.Widget.Layout
21+import Reflex.Vty.Widget.Input.Mouse
21
22
22-- | Configuration options for a 'textInput'. For more information on
23-- | Configuration options for a 'textInput'. For more information on
23-- 'TextZipper', see 'Data.Text.Zipper'.
24-- 'TextZipper', see 'Data.Text.Zipper'.
@@ -43,7 +44,7 @@ data TextInput t = TextInput
43
44
44-- | A widget that allows text input
45-- | A widget that allows text input
45textInput
46textInput
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)
47 => TextInputConfig t
48 => TextInputConfig t
48 -> m (TextInput t)
49 -> m (TextInput t)
49textInput cfg = do
50textInput cfg = do
@@ -81,7 +82,7 @@ textInput cfg = do
81
82
82-- | A widget that allows multiline text input
83-- | A widget that allows multiline text input
83multilineTextInput
84multilineTextInput
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)
85 => TextInputConfig t
86 => TextInputConfig t
86 -> m (TextInput t)
87 -> m (TextInput t)
87multilineTextInput cfg = do
88multilineTextInput cfg = do
@@ -99,7 +100,7 @@ multilineTextInput cfg = do
99-- the computed line count to greedily size the tile when vertically
100-- the computed line count to greedily size the tile when vertically
100-- oriented, and uses the fallback width when horizontally oriented.
101-- oriented, and uses the fallback width when horizontally oriented.
101textInputTile
102textInputTile
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)
103 => m (TextInput t)
104 => m (TextInput t)
104 -> Dynamic t Int
105 -> Dynamic t Int
105 -> m (TextInput t)
106 -> m (TextInput t)
src/Reflex/Vty/Widget/Layout.hs+19−31
@@ -23,6 +23,7 @@ import qualified Graphics.Vty as V
23import Reflex
23import Reflex
24import Reflex.Host.Class (MonadReflexCreateTrigger)
24import Reflex.Host.Class (MonadReflexCreateTrigger)
25import Reflex.Vty.Widget
25import Reflex.Vty.Widget
26+import Reflex.Vty.Widget.Input.Mouse
26
27
27-- * Focus
28-- * Focus
28--
29--
@@ -34,7 +35,7 @@ import Reflex.Vty.Widget
34-- a tab press in a sibling element, a click event).
35-- a tab press in a sibling element, a click event).
35--
36--
36-- Focusable elements will usually be created via 'tile', but can also be
37-- 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
38-- more find-grained control of focus behavior.
39-- more find-grained control of focus behavior.
39
40
40-- ** Storing focus state
41-- ** Storing focus state
@@ -75,7 +76,7 @@ shiftFS (FocusSet s) fid n = case OSet.findIndex <$> fid <*> pure s of
75-- ** The focus management monad
76-- ** The focus management monad
76
77
77-- | A class for things that can produce focusable elements.
78-- | 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
79 -- | Create a focusable element.
80 -- | Create a focusable element.
80 makeFocus :: m FocusId
81 makeFocus :: m FocusId
81 -- | Emit an 'Event' of requests to change the focus.
82 -- | 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)
124 traverseDMapWithKeyWithAdjust f m e = Focus $ traverseDMapWithKeyWithAdjust (\k v -> unFocus $ f k v) m e
125 traverseDMapWithKeyWithAdjust f m e = Focus $ traverseDMapWithKeyWithAdjust (\k v -> unFocus $ f k v) m e
125 traverseDMapWithKeyWithAdjustWithMove f m e = Focus $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unFocus $ f k v) m e
126 traverseDMapWithKeyWithAdjustWithMove f m e = Focus $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unFocus $ f k v) m e
126
127
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
128 localInput f = hoist (localInput f)
129 localInput f = hoist (localInput f)
129
130
130−instance (HasVtyWidgetCtx t m, Reflex t, MonadFix m) => HasVtyWidgetCtx t (Focus t m) where
131− localCtx f = hoist (localCtx f)
132−
133instance (HasImageWriter t m, MonadFix m) => HasImageWriter t (Focus t m) where
131instance (HasImageWriter t m, MonadFix m) => HasImageWriter t (Focus t m) where
134 mapImages f = hoist (mapImages f)
132 mapImages f = hoist (mapImages f)
135
133
136−instance (HasFocus t m, Monad m) => HasFocus t (Focus t m)
134+instance (HasFocusReader t m, Monad m) => HasFocusReader t (Focus t m)
137
135
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
139 makeFocus = do
137 makeFocus = do
140 fid <- FocusId <$> lift getNextNodeId
138 fid <- FocusId <$> lift getNextNodeId
141 Focus $ tellDyn $ pure $ singletonFS fid
139 Focus $ tellDyn $ pure $ singletonFS fid
@@ -171,7 +169,7 @@ runFocus (Focus x) = do
171-- | Runs an action in the focus monad, providing it with information about
169-- | Runs an action in the focus monad, providing it with information about
172-- whether any of the foci created within it are focused.
170-- whether any of the foci created within it are focused.
173anyChildFocused
171anyChildFocused
174− :: (MonadFocus t m, MonadFix m)
172+ :: (HasFocus t m, MonadFix m)
175 => (Dynamic t Bool -> m a)
173 => (Dynamic t Bool -> m a)
176 -> m a
174 -> m a
177anyChildFocused f = do
175anyChildFocused f = do
@@ -186,7 +184,7 @@ anyChildFocused f = do
186
184
187-- | Request focus be shifted backward and forward based on tab presses. <Tab>
185-- | Request focus be shifted backward and forward based on tab presses. <Tab>
188-- shifts focus forward and <Shift+Tab> shifts focus backward.
186-- 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 ()
190tabNavigation = do
188tabNavigation = do
191 fwd <- fmap (const 1) <$> key (V.KChar '\t')
189 fwd <- fmap (const 1) <$> key (V.KChar '\t')
192 back <- fmap (const (-1)) <$> key V.KBackTab
190 back <- fmap (const (-1)) <$> key V.KBackTab
@@ -244,14 +242,14 @@ data Orientation = Orientation_Column
244
242
245-- | Create a row-oriented 'axis'
243-- | Create a row-oriented 'axis'
246row
244row
247− :: (Reflex t, MonadNodeId m, MonadFix m, MonadLayout t m)
245+ :: (Reflex t, MonadNodeId m, MonadFix m, HasLayout t m)
248 => m a
246 => m a
249 -> m a
247 -> m a
250row = axis (pure Orientation_Row) flex
248row = axis (pure Orientation_Row) flex
251
249
252-- | Create a column-oriented 'axis'
250-- | Create a column-oriented 'axis'
253col
251col
254− :: (Reflex t, MonadNodeId m, MonadFix m, MonadLayout t m)
252+ :: (Reflex t, MonadNodeId m, MonadFix m, HasLayout t m)
255 => m a
253 => m a
256 -> m a
254 -> m a
257col = axis (pure Orientation_Column) flex
255col = axis (pure Orientation_Column) flex
@@ -362,7 +360,7 @@ chunk o r (offset, sz) = case o of
362-- ** The layout monad
360-- ** The layout monad
363
361
364-- | A class of operations for creating screen layouts.
362-- | 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
366 -- | Creates a parent element in the current layout with the given size
364 -- | Creates a parent element in the current layout with the given size
367 -- constraint, which lays out its children according to the provided
365 -- constraint, which lays out its children according to the provided
368 -- orientation.
366 -- orientation.
@@ -408,16 +406,6 @@ instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (Layout t m
408 traverseDMapWithKeyWithAdjust f m e = Layout $ traverseDMapWithKeyWithAdjust (\k v -> unLayout $ f k v) m e
406 traverseDMapWithKeyWithAdjust f m e = Layout $ traverseDMapWithKeyWithAdjust (\k v -> unLayout $ f k v) m e
409 traverseDMapWithKeyWithAdjustWithMove f m e = Layout $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unLayout $ f k v) m e
407 traverseDMapWithKeyWithAdjustWithMove f m e = Layout $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unLayout $ f k v) m e
410
408
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−
421-- | Apply a transformation to the context of a child 'Layout' action and run
409-- | Apply a transformation to the context of a child 'Layout' action and run
422-- that action
410-- that action
423hoistRunLayout
411hoistRunLayout
@@ -434,15 +422,15 @@ hoistRunLayout f x = do
434 let reg = Region 0 0 <$> dw <*> dh
422 let reg = Region 0 0 <$> dw <*> dh
435 runLayout orientation reg x
423 runLayout orientation reg x
436
424
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
438 localInput = hoistRunLayout . localInput
426 localInput = hoistRunLayout . localInput
439
427
440instance (HasDisplayRegion t m, HasImageWriter t m, MonadFix m) => HasImageWriter t (Layout t m) where
428instance (HasDisplayRegion t m, HasImageWriter t m, MonadFix m) => HasImageWriter t (Layout t m) where
441 mapImages f = hoistRunLayout (mapImages f)
429 mapImages f = hoistRunLayout (mapImages f)
442
430
443−instance (HasFocus t m, Monad m) => HasFocus t (Layout t m)
431+instance (HasFocusReader t m, Monad m) => HasFocusReader t (Layout t m)
444
432
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
446 axis o c (Layout x) = Layout $ do
434 axis o c (Layout x) = Layout $ do
447 nodeId <- getNextNodeId
435 nodeId <- getNextNodeId
448 (result, forest) <- lift $ local (\t -> fromMaybe (LayoutTree (Region 0 0 0 0, Orientation_Column) mempty) . lookupLF nodeId . childrenLT <$> t) $ runDynamicWriterT x
436 (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
455 pure $ maybe (Region 0 0 0 0) (fst . rootLT) . lookupLF nodeId . childrenLT <$> solutions
443 pure $ maybe (Region 0 0 0 0) (fst . rootLT) . lookupLF nodeId . childrenLT <$> solutions
456 askOrientation = Layout $ asks $ fmap (snd . rootLT)
444 askOrientation = Layout $ asks $ fmap (snd . rootLT)
457
445
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
459 makeFocus = lift makeFocus
447 makeFocus = lift makeFocus
460 requestFocus = lift . requestFocus
448 requestFocus = lift . requestFocus
461 isFocused = lift . isFocused
449 isFocused = lift . isFocused
@@ -490,7 +478,7 @@ initLayout f = do
490-- * The tile "window manager"
478-- * The tile "window manager"
491--
479--
492-- $tiling
480-- $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
494-- interface. These functions check the available screen size and initialize
482-- interface. These functions check the available screen size and initialize
495-- the layout monad with that information, and also initialize the focus monad.
483-- the layout monad with that information, and also initialize the focus monad.
496
484
@@ -517,7 +505,7 @@ initManager_ = fmap fst . initManager
517-- provided constraint. Returns the 'FocusId' allowing for manual focus
505-- provided constraint. Returns the 'FocusId' allowing for manual focus
518-- management.
506-- management.
519tile'
507tile'
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)
521 => Dynamic t Constraint
509 => Dynamic t Constraint
522 -> m a
510 -> m a
523 -> m (FocusId, a)
511 -> m (FocusId, a)
@@ -536,7 +524,7 @@ tile' c w = do
536-- | A widget that is focusable and occupies a layout region based on the
524-- | A widget that is focusable and occupies a layout region based on the
537-- provided constraint.
525-- provided constraint.
538tile
526tile
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)
540 => Dynamic t Constraint
528 => Dynamic t Constraint
541 -> m a
529 -> m a
542 -> m a
530 -> m a
@@ -547,7 +535,7 @@ tile c = fmap snd . tile' c
547-- | A widget that is not focusable and occupies a layout region based on the
535-- | A widget that is not focusable and occupies a layout region based on the
548-- provided constraint.
536-- provided constraint.
549grout
537grout
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)
551 => Dynamic t Constraint
539 => Dynamic t Constraint
552 -> m a
540 -> m a
553 -> m a
541 -> 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