on develop help sign in
17 files changed+651−409
since splitsnapshots
unifiedsplit
.github/workflows/haskell.yml+12−10 .travis.yml−112 ChangeLog.md+26−11 reflex-vty.cabal+25−8 src-bin/Example/CPU.hs+2−4 src-bin/example.hs+12−6 src/Data/Text/Zipper.hs+280−157 src/Reflex/Class/Switchable.hs−27 src/Reflex/Vty/Widget.hs+97−14 src/Reflex/Vty/Widget/Box.hs+19−17 src/Reflex/Vty/Widget/Input.hs+9−8 src/Reflex/Vty/Widget/Input/Text.hs+16−12 src/Reflex/Vty/Widget/Layout.hs+14−9 src/Reflex/Vty/Widget/Split.hs+3−4 src/Reflex/Vty/Widget/Text.hs+17−10 test/Data/Text/ZipperSpec.hs+104 test/Spec.hs+15
.github/workflows/haskell.yml+12−10
@@ -1,33 +1,33 @@
1−name: Haskell CI
1+name: github-action
22
33on: [push, pull_request]
44
55jobs:
66 build:
7−
8− runs-on: ${{ matrix.os }}
97 strategy:
108 matrix:
11− ghc: ['8.4.4', '8.6.5', '8.8.4']
9+ ghc: ['8.4.4', '8.6.5', '8.8.4', '8.10.2']
1210 os: ['ubuntu-latest', 'macos-latest']
11+ runs-on: ${{ matrix.os }}
12+
13+ name: GHC ${{ matrix.ghc }} on ${{ matrix.os }}
1314 steps:
1415 - uses: actions/checkout@v2
1516 - uses: actions/setup-haskell@v1
1617 with:
1718 ghc-version: ${{ matrix.ghc }}
18− cabal-version: '3.0'
19−
2019 - name: Cache
2120 uses: actions/cache@v1
2221 env:
2322 cache-name: cache-cabal
2423 with:
2524 path: ~/.cabal
26− key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/*.cabal') }}-${{ hashFiles('**/cabal.project') }}
25+ key: ${{ runner.os }}-${{ matrix.ghc }}-build-${{ env.cache-name }}-${{ hashFiles('**/*.cabal') }}-${{ hashFiles('**/cabal.project') }}
2726 restore-keys: |
28− ${{ runner.os }}-build-${{ env.cache-name }}-
29− ${{ runner.os }}-build-
30− ${{ runner.os }}-
27+ ${{ runner.os }}-${{ matrix.ghc }}-build-${{ env.cache-name }}-
28+ ${{ runner.os }}-${{ matrix.ghc }}-build-
29+ ${{ runner.os }}-${{ matrix.ghc }}-
30+ ${{ runner.os }}
3131
3232 - name: Install dependencies
3333 run: |
@@ -37,3 +37,5 @@ jobs:
3737 run: cabal build --enable-tests --enable-benchmarks all
3838 - name: Run tests
3939 run: cabal test all
40+ - name: Build Docs
41+ run: cabal haddock
.travis.ymldeleted−112
@@ -1,112 +0,0 @@
1−# This Travis job script has been generated by a script via
2−#
3−# haskell-ci 'reflex-vty.cabal' '--output' '.travis.yml'
4−#
5−# For more information, see https://github.com/haskell-CI/haskell-ci
6−#
7−# version: 0.3
8−#
9−language: c
10−dist: xenial
11−
12−git:
13− submodules: false # whether to recursively clone submodules
14−
15−cache:
16− directories:
17− - $HOME/.cabal/packages
18− - $HOME/.cabal/store
19−
20−before_cache:
21− - rm -fv $CABALHOME/packages/hackage.haskell.org/build-reports.log
22− # remove files that are regenerated by 'cabal update'
23− - rm -fv $CABALHOME/packages/hackage.haskell.org/00-index.*
24− - rm -fv $CABALHOME/packages/hackage.haskell.org/*.json
25− - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.cache
26− - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar
27− - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar.idx
28−
29− - rm -rfv $CABALHOME/packages/head.hackage
30−
31−matrix:
32− include:
33− - compiler: "ghc-8.8.3"
34− addons: {apt: {packages: [ghc-ppa-tools,cabal-install-3.0,ghc-8.8.3], sources: [hvr-ghc]}}
35− - compiler: "ghc-8.6.5"
36− addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-8.6.5], sources: [hvr-ghc]}}
37− - compiler: "ghc-8.4.4"
38− addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-8.4.4], sources: [hvr-ghc]}}
39−
40−
41−before_install:
42− - HC=/opt/ghc/bin/${CC}
43− - HCPKG=${HC/ghc/ghc-pkg}
44− - unset CC
45− - CABAL=/opt/ghc/bin/cabal
46− - CABALHOME=$HOME/.cabal
47− - export PATH="$CABALHOME/bin:$PATH"
48− - ROOTDIR=$(pwd)
49− - HCNUMVER=$(( $(${HC} --numeric-version|sed -E 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\1 * 10000 + \2 * 100 + \3/') ))
50− - echo $HCNUMVER
51−
52−install:
53− - ${CABAL} --version
54− - echo "$(${HC} --version) [$(${HC} --print-project-git-commit-id 2> /dev/null || echo '?')]"
55− - TEST=--enable-tests
56− - BENCH=--enable-benchmarks
57− - GHCHEAD=${GHCHEAD-false}
58− - travis_retry ${CABAL} update -v
59− - sed -i.bak 's/^jobs:/-- jobs:/' $CABALHOME/config
60− - rm -fv cabal.project cabal.project.local
61− - grep -Ev -- '^\s*--' $CABALHOME/config | grep -Ev '^\s*$'
62− - rm -f cabal.project
63− - touch cabal.project
64− - "printf 'packages: \".\"\\n' >> cabal.project"
65− - "printf 'write-ghc-environment-files: always\\n' >> cabal.project"
66− - touch cabal.project.local
67− - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | grep -vE -- '^(reflex-vty)$' | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done"
68− - cat cabal.project || true
69− - cat cabal.project.local || true
70− - if [ -f "./configure.ac" ]; then (cd "." && autoreconf -i); fi
71− - rm -f cabal.project.freeze
72− - ${CABAL} new-freeze -w ${HC} ${TEST} ${BENCH} --project-file="cabal.project" --dry
73− - "cat \"cabal.project.freeze\" | sed -E 's/^(constraints: *| *)//' | sed 's/any.//'"
74− - rm "cabal.project.freeze"
75− - ${CABAL} new-build -w ${HC} ${TEST} ${BENCH} --project-file="cabal.project" --dep -j2 all
76− - ${CABAL} new-build -w ${HC} --disable-tests --disable-benchmarks --project-file="cabal.project" --dep -j2 all
77− - rm -rf .ghc.environment.* "."/dist
78− - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)
79−
80−# Here starts the actual work to be performed for the package under test;
81−# any command which exits with a non-zero exit code causes the build to fail.
82−script:
83− # test that source-distributions can be generated
84− - ${CABAL} new-sdist all
85− - mv dist-newstyle/sdist/*.tar.gz ${DISTDIR}/
86− - cd ${DISTDIR} || false
87− - find . -maxdepth 1 -name '*.tar.gz' -exec tar -xvf '{}' \;
88− - rm -f cabal.project
89− - touch cabal.project
90− - "printf 'packages: \"reflex-vty-*/*.cabal\"\\n' >> cabal.project"
91− - "printf 'write-ghc-environment-files: always\\n' >> cabal.project"
92− - touch cabal.project.local
93− - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | grep -vE -- '^(reflex-vty)$' | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done"
94− - cat cabal.project || true
95− - cat cabal.project.local || true
96− # this builds all libraries and executables (without tests/benchmarks)
97− - ${CABAL} new-build -w ${HC} --disable-tests --disable-benchmarks all
98−
99− # build & run tests, build benchmarks
100− - ${CABAL} new-build -w ${HC} ${TEST} ${BENCH} all
101−
102− # cabal check
103− - (cd reflex-vty-* && ${CABAL} check)
104−
105− # haddock
106− - ${CABAL} new-haddock -w ${HC} ${TEST} ${BENCH} all
107−
108− # Build without installed constraints for packages in global-db
109− - rm -f cabal.project.local; ${CABAL} new-build -w ${HC} --disable-tests --disable-benchmarks all
110−
111−# REGENDATA ["reflex-vty.cabal","--output",".travis.yml"]
112−# EOF
ChangeLog.md+26−11
@@ -33,6 +33,8 @@
3333 * Introduce a `FocusReader` monad transformer
3434 * Replace `HasVtyInput` with `HasInput`
3535 * Introduce an `Input` monad transformer
36+ * Introduce `HasTheme` reader class to allow setting Vty attributes of all built-in widgets
37+ * Introduce `ThemeReader` monad transformer
3638 * Remove `DynRegion` and `currentRegion`. Use `Dynamic t Region` and `current` instead. This also changes the type of `pane`'s argument.
3739 * `CheckboxConfig` now has a field taking an `Event` to set the value of the checkbox.
3840 * `checkbox` now accepts keyboard input (spacebar to check and uncheck) and is displayed in bold when focused.
@@ -54,21 +56,28 @@
5456 * `splitH`: Now requires `HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m`
5557 * `splitVDrag`: Now requires `HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m`
5658 * `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`
59+ * `boxTitle`: Now requires `HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m, HasTheme t m`
60+ * `box`: Now requires `HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m, HasTheme t m`
61+ * `boxStatic`: Now requires `HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m, HasTheme t m`
6062 * `richText`: Now requires `HasImageWriter`, and `HasDisplayRegion`
61− * `scrollableText`: Now requires `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
63+ * `scrollableText`: Now requires `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
6264 * `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`
65+ * `button`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
66+ * `textButton`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
67+ * `textButtonStatic`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
68+ * `link`: Now requires `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
6769 * `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`
70+ * TextZipper interface changes
71+ * `_displayLines_offsetMap` type changed to `OffsetMapWithAlignment`
72+ * `_displayLines_cursorY` replaced with `_displayLines_cursorPos` which include X position
73+ * some exposed methods intended for internal use only have been removed
74+ * `textInput`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
75+ * `multilineTextInput`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
76+ * `textInputTile`: Now requires `HasFocusReader`, `HasInput`, `HasLayout`, `HasTheme`, and `HasFocus`
7177* _Misc_:
78+ * (#40 Add alignment support to TextZipper)[https://github.com/reflex-frp/reflex-vty/pull/40]
79+ * Add alignment (left/center/right) support to TextZipper
80+ * Add basic unit tests for newly created alignment methods in TextZipper
7281 * Add default instances for `HasInput`, `HasFocus`, and `HasImageWriter`
7382 * Export `withinImage` and add `imagesInRegion` to crop images to a region
7483 * Add `anyChildFocused`, which provides information about whether subwidgets are focused
@@ -77,6 +86,12 @@
7786 * Add various `MFunctor` instances
7887 * Add a CPU usage indicator to the example executable
7988
89+## 0.1.4.2
90+
91+* Wider bounds for GHC 8.10 support
92+
93+## 0.1.4.1
94+
8095## 0.1.4.1
8196* Migrate to new dependent-sum / dependent-map (after the "some" package split)
8297
reflex-vty.cabal+25−8
@@ -12,12 +12,17 @@ maintainer: maintainer@obsidian.systems
1212copyright: 2020 Obsidian Systems LLC
1313category: FRP
1414build-type: Simple
15−cabal-version: >=1.18
15+cabal-version: 1.18
1616extra-source-files:
1717 README.md
1818 ChangeLog.md
1919extra-doc-files: doc/tasks.png
20−tested-with: GHC ==8.8.4 || ==8.6.5 || ==8.4.4
20+tested-with:
21+ GHC ==8.4.4 || ==8.6.5 || ==8.8.4 || ==8.10.2
22+
23+source-repository head
24+ type: git
25+ location: https://github.com/reflex-frp/reflex-vty
2126
2227library
2328 exposed-modules: Reflex.Vty
@@ -31,12 +36,11 @@ library
3136 , Reflex.Vty.Widget.Split
3237 , Reflex.Vty.Widget.Text
3338 , Data.Text.Zipper
34− , Reflex.Class.Switchable
3539 , Reflex.Spider.Orphans
3640 , Control.Monad.NodeId
3741 build-depends:
38− base >= 4.10.0 && < 4.14,
39− bimap >= 0.3.3 && < 0.4,
42+ base >= 4.10.0 && < 4.15,
43+ bimap >= 0.3.3 && < 0.5,
4044 containers >= 0.5.0 && < 0.7,
4145 mtl >= 2.2.2 && < 2.3,
4246 transformers >= 0.5.5 && < 0.6,
@@ -132,6 +136,19 @@ executable example
132136 TypeFamilies
133137
134138
135−source-repository head
136− type: git
137− location: https://github.com/reflex-frp/reflex-vty
139+test-suite reflex-vty-test
140+ hs-source-dirs: test
141+ main-is: Spec.hs
142+ type: exitcode-stdio-1.0
143+ ghc-options: -threaded
144+ other-modules:
145+ Data.Text.ZipperSpec
146+ build-depends:
147+ base,
148+ reflex,
149+ containers,
150+ reflex-vty,
151+ text,
152+ extra,
153+ hspec >= 2.7 && < 2.8
154+ default-language: Haskell2010
src-bin/Example/CPU.hs+2−4
@@ -99,9 +99,8 @@ cpuStats
9999 , HasImageWriter t m
100100 , HasLayout t m
101101 , HasFocus t m
102− , MonadNodeId m
103102 , HasInput t m
104− , HasFocusReader t m
103+ , HasFocusReader t m, HasTheme t m
105104 )
106105 => m ()
107106cpuStats = do
@@ -120,11 +119,10 @@ chart
120119 :: ( MonadFix m
121120 , HasFocus t m
122121 , HasLayout t m
123− , MonadNodeId m
124122 , HasImageWriter t m
125123 , HasInput t m
126124 , HasDisplayRegion t m
127− , HasFocusReader t m
125+ , HasFocusReader t m, HasTheme t m
128126 )
129127 => Dynamic t (Ratio Word64) -> m ()
130128chart pct = do
src-bin/example.hs+12−6
@@ -1,7 +1,6 @@
11import Control.Applicative
22import Control.Monad
33import Control.Monad.Fix
4−import Control.Monad.NodeId
54import Data.Functor
65import Data.Functor.Misc
76import Data.Map (Map)
@@ -22,10 +21,9 @@ type VtyExample t m =
2221 , Reflex t
2322 , HasInput t m
2423 , HasImageWriter t m
25− , MonadNodeId m
2624 , HasDisplayRegion t m
2725 , HasFocus t m
28− , HasFocusReader t m
26+ , HasFocusReader t m, HasTheme t m
2927 )
3028
3129type Manager t m =
@@ -48,12 +46,20 @@ withCtrlC f = do
4846 V.EvKey (V.KChar 'c') [V.MCtrl] -> Just ()
4947 _ -> Nothing
5048
49+darkTheme :: V.Attr
50+darkTheme = V.Attr {
51+ V.attrStyle = V.SetTo V.standout
52+ , V.attrForeColor = V.SetTo V.black
53+ , V.attrBackColor = V.Default
54+ , V.attrURL = V.Default
55+}
56+
5157main :: IO ()
5258main = mainWidget $ withCtrlC $ do
5359 initManager_ $ do
5460 tabNavigation
5561 let gf = grout . fixed
56− t = tile flex
62+ t = tile flex
5763 buttons = col $ do
5864 gf 3 $ col $ do
5965 gf 1 $ text "Select an example."
@@ -79,7 +85,7 @@ main = mainWidget $ withCtrlC $ do
7985 _ -> Nothing
8086 rec out <- networkHold buttons $ ffor (switch (current out)) $ \case
8187 Left Example_Todo -> escapable taskList
82− Left Example_TextEditor -> escapable testBoxes
88+ Left Example_TextEditor -> escapable $ localTheme (const (constant darkTheme)) testBoxes
8389 Left Example_ScrollableTextDisplay -> escapable scrolling
8490 Left Example_ClickButtonsGetEmojis -> escapable easyExample
8591 Left Example_CPUStat -> escapable cpuStats
@@ -235,7 +241,7 @@ scrolling = col $ do
235241-- * Text editor example with resizable boxes
236242
237243testBoxes
238− :: (MonadHold t m, MonadNodeId m, VtyExample t m)
244+ :: (MonadHold t m, VtyExample t m)
239245 => m ()
240246testBoxes = do
241247 dw <- displayWidth
src/Data/Text/Zipper.hs+280−157
@@ -9,22 +9,26 @@ Description: A zipper for text documents that allows convenient editing and navi
99-}
1010module Data.Text.Zipper where
1111
12+import Prelude
13+
14+import Control.Exception (assert)
15+import Control.Monad.State (evalState, forM, get, put)
1216import Data.Char (isSpace)
1317import Data.Map (Map)
14−import qualified Data.Map as Map
1518import Data.Maybe (fromMaybe)
1619import Data.String
17−import Control.Monad.State (evalState, forM, get, put)
18−
1920import Data.Text (Text)
20−import qualified Data.Text as T
2121import Data.Text.Internal (Text(..), text)
2222import Data.Text.Internal.Fusion (stream)
2323import Data.Text.Internal.Fusion.Types (Stream(..), Step(..))
2424import Data.Text.Unsafe
25+import qualified Data.List as L
26+import qualified Data.Map as Map
27+import qualified Data.Text as T
2528
2629import Graphics.Text.Width (wcwidth)
2730
31+
2832-- | A zipper of the logical text input contents (the "document"). The lines
2933-- before the line containing the cursor are stored in reverse order.
3034-- The cursor is logically between the "before" and "after" text.
@@ -166,7 +170,7 @@ deleteLeftWord (TextZipper lb b a la) =
166170-- | Insert up to n spaces to get to the next logical column that is a multiple of n
167171tab :: Int -> TextZipper -> TextZipper
168172tab n z@(TextZipper _ b _ _) =
169− insert (T.replicate (fromEnum $ n - (T.length b `mod` max 1 n)) " ") z
173+ insert (T.replicate (fromEnum $ n - T.length b `mod` max 1 n) " ") z
170174
171175-- | The plain text contents of the zipper
172176value :: TextZipper -> Text
@@ -189,105 +193,35 @@ fromText = flip insert empty
189193data Span tag = Span tag Text
190194 deriving (Show)
191195
196+-- | Text alignment type
197+data TextAlignment =
198+ TextAlignment_Left
199+ | TextAlignment_Right
200+ | TextAlignment_Center
201+ deriving (Eq, Show)
202+
203+-- A map from the index (row) of display line to (fst,snd)
204+-- fst: leading empty spaces from left (may be negative) to adjust for alignment
205+-- snd: the text offset from the beginning of the document
206+-- to the first character of the display line
207+type OffsetMapWithAlignment = Map Int (Int, Int)
208+
209+-- helper type representing a single visual line that may be part of a wrapped logical line
210+data WrappedLine = WrappedLine
211+ { _wrappedLines_text :: Text
212+ , _wrappedLines_hiddenWhitespace :: Bool -- ^ 'True' if this line ends with a deleted whitespace character
213+ , _wrappedLines_offset :: Int -- ^ offset from beginning of line
214+ }
215+ deriving (Eq, Show)
216+
192217-- | Information about the document as it is displayed (i.e., post-wrapping)
193218data DisplayLines tag = DisplayLines
194219 { _displayLines_spans :: [[Span tag]]
195− , _displayLines_offsetMap :: Map Int Int
196− , _displayLines_cursorY :: Int
220+ , _displayLines_offsetMap :: OffsetMapWithAlignment
221+ , _displayLines_cursorPos :: (Int, Int) -- cursor position relative to upper left hand corner
197222 }
198223 deriving (Show)
199224
200−-- | Given a width and a 'TextZipper', produce a list of display lines
201−-- (i.e., lines of wrapped text) with special attributes applied to
202−-- certain segments (e.g., the cursor). Additionally, produce the current
203−-- y-coordinate of the cursor and a mapping from display line number to text
204−-- offset
205−displayLines
206− :: Int -- ^ Width, used for wrapping
207− -> tag -- ^ Metadata for normal characters
208− -> tag -- ^ Metadata for the cursor
209− -> TextZipper -- ^ The text input contents and cursor state
210− -> DisplayLines tag
211−displayLines width tag cursorTag (TextZipper lb b a la) =
212− let linesBefore :: [[Text]] -- The wrapped lines before the cursor line
213− linesBefore = map (wrapWithOffset width 0) $ reverse lb
214− linesAfter :: [[Text]] -- The wrapped lines after the cursor line
215− linesAfter = map (wrapWithOffset width 0) la
216− offsets :: Map Int Int
217− offsets = offsetMap $ mconcat
218− [ linesBefore
219− , [wrapWithOffset width 0 $ b <> a]
220− , linesAfter
221− ]
222− spansBefore = map ((:[]) . Span tag) $ concat linesBefore
223− spansAfter = map ((:[]) . Span tag) $ concat linesAfter
224− -- Separate the spans before the cursor into
225− -- * spans that are on earlier display lines (though on the same logical line), and
226− -- * spans that are on the same display line
227− (spansCurrentBefore, spansCurLineBefore) = fromMaybe ([], []) $
228− initLast $ map ((:[]) . Span tag) (wrapWithOffset width 0 b)
229− -- Calculate the number of columns on the cursor's display line before the cursor
230− curLineOffset = spansWidth spansCurLineBefore
231− -- Check whether the spans on the current display line are long enough that
232− -- the cursor has to go to the next line
233− cursorAfterEOL = curLineOffset == width
234− cursorCharWidth = case T.uncons a of
235− Nothing -> 1
236− Just (c, _) -> charWidth c
237− -- Separate the span after the cursor into
238− -- * spans that are on the same display line, and
239− -- * spans that are on later display lines (though on the same logical line)
240− (spansCurLineAfter, spansCurrentAfter) = fromMaybe ([], []) $
241− headTail $ case T.uncons a of
242− Nothing -> [[Span cursorTag " "]]
243− Just (c, rest) ->
244− let o = if cursorAfterEOL then cursorCharWidth else curLineOffset + cursorCharWidth
245− cursor = Span cursorTag (T.singleton c)
246− in case map ((:[]) . Span tag) (wrapWithOffset width o rest) of
247− [] -> [[cursor]]
248− (l:ls) -> (cursor : l) : ls
249− in DisplayLines
250− { _displayLines_spans = concat
251− [ spansBefore
252− , spansCurrentBefore
253− , if cursorAfterEOL
254− then [ spansCurLineBefore, spansCurLineAfter ]
255− else [ spansCurLineBefore <> spansCurLineAfter ]
256− , spansCurrentAfter
257− , spansAfter
258− ]
259− , _displayLines_offsetMap = offsets
260− , _displayLines_cursorY = sum
261− [ length spansBefore
262− , length spansCurrentBefore
263− , if cursorAfterEOL then 1 else 0
264− ]
265− }
266− where
267− initLast :: [a] -> Maybe ([a], a)
268− initLast = \case
269− [] -> Nothing
270− (x:xs) -> case initLast xs of
271− Nothing -> Just ([], x)
272− Just (ys, y) -> Just (x:ys, y)
273− headTail :: [a] -> Maybe (a, [a])
274− headTail = \case
275− [] -> Nothing
276− x:xs -> Just (x, xs)
277−
278−-- | Wraps a logical line of text to fit within the given width. The first
279−-- wrapped line is offset by the number of columns provided. Subsequent wrapped
280−-- lines are not.
281−wrapWithOffset
282− :: Int -- ^ Maximum width
283− -> Int -- ^ Offset for first line
284− -> Text -- ^ Text to be wrapped
285− -> [Text]
286−wrapWithOffset maxWidth _ _ | maxWidth <= 0 = []
287−wrapWithOffset maxWidth n xs =
288− let (firstLine, rest) = splitAtWidth (maxWidth - n) xs
289− in firstLine : (fmap (takeWidth maxWidth) . takeWhile (not . T.null) . iterate (dropWidth maxWidth) $ rest)
290−
291225-- | Split a 'Text' at the given column index. For example
292226--
293227-- > splitAtWidth 3 "ᄀabc" == ("ᄀa", "bc")
@@ -297,16 +231,16 @@ splitAtWidth :: Int -> Text -> (Text, Text)
297231splitAtWidth n t@(Text arr off len)
298232 | n <= 0 = (T.empty, t)
299233 | n >= textWidth t = (t, T.empty)
300− | otherwise = let k = iterNWidth n t
234+ | otherwise = let k = toLogicalIndex n t
301235 in (text arr off k, text arr (off+k) (len-k))
302− where
303− iterNWidth :: Int -> Text -> Int
304− iterNWidth n' t'@(Text _ _ len') = loop 0 0
305− where loop !i !cnt
306− | i >= len' || cnt + w > n' = i
307− | otherwise = loop (i+d) (cnt + w)
308− where Iter c d = iter t' i
309− w = charWidth c
236+
237+toLogicalIndex :: Int -> Text -> Int
238+toLogicalIndex n' t'@(Text _ _ len') = loop 0 0
239+ where loop !i !cnt
240+ | i >= len' || cnt + w > n' = i
241+ | otherwise = loop (i+d) (cnt + w)
242+ where Iter c d = iter t' i
243+ w = charWidth c
310244
311245-- | Takes the given number of columns of characters. For example
312246--
@@ -335,56 +269,6 @@ dropWidth n = snd . splitAtWidth n
335269charWidth :: Char -> Int
336270charWidth = wcwidth
337271
338−
339−-- | For a given set of wrapped logical lines, computes a map
340−-- from display line index to text offset in the original text.
341−-- This is used to help determine how interactions with the displayed
342−-- text map back to the original text.
343−-- For example, given the document @\"AA\\nBBB\\nCCCCCCCC\\n\"@ wrapped to 5 columns,
344−-- this function will compute the offset in the original document of each character
345−-- in column 1:
346−--
347−-- > AA... (0, 0)
348−-- > BBB.. (1, 3)
349−-- > CCCCC (2, 7) -- (this line wraps to the next row)
350−-- > CCC.. (3, 12)
351−-- > ..... (4, 16)
352−offsetMap
353− :: [[Text]] -- ^ The outer list represents logical lines, and the
354− -- inner list represents the display lines into which
355− -- the logical line has been wrapped
356− -> Map Int Int -- ^ A map from the index (row) of display line to
357− -- the text offset from the beginning of the document
358− -- to the first character of the display line
359−offsetMap ts = evalState (offsetMap' ts) (0, 0)
360− where
361− offsetMap' xs = fmap Map.unions $ forM xs $ \x -> do
362− maps <- forM x $ \line -> do
363− let l = T.length line
364− (dl, o) <- get
365− put (dl + 1, o + l)
366− return $ Map.singleton dl o
367− (dl, o) <- get
368− put (dl, o + 1)
369− return $ Map.insert dl (o + 1) $ Map.unions maps
370−
371−-- | Move the cursor of the given 'TextZipper' to the logical position indicated
372−-- by the given display line coordinates, using the provided 'DisplayLines'
373−-- information. If the x coordinate is beyond the end of a line, the cursor is
374−-- moved to the end of the line.
375−goToDisplayLinePosition :: Int -> Int -> DisplayLines tag -> TextZipper -> TextZipper
376−goToDisplayLinePosition x y dl tz =
377− let offset = Map.lookup y $ _displayLines_offsetMap dl
378− in case offset of
379− Nothing -> tz
380− Just o ->
381− let
382− moveRight = case drop y $ _displayLines_spans dl of
383− [] -> x
384− (s:_) -> charIndexAt x . stream . mconcat . fmap (\(Span _ t) -> t) $ s
385− in rightN (o + min moveRight x) $ top tz
386−
387−
388272-- | Get the width of the text in a set of 'Span's, taking into account unicode character widths
389273spansWidth :: [Span tag] -> Int
390274spansWidth = sum . map (\(Span _ t) -> textWidth t)
@@ -420,3 +304,242 @@ charIndexAt pos (Stream next s0 _len) = loop_length 0 0 s0
420304 Yield c s' -> if w > pos then i else loop_length (i+1) w s' where
421305 w = z + charWidth c
422306{-# INLINE[0] charIndexAt #-}
307+
308+
309+
310+
311+-- | Same as T.words except whitespace characters are included at end (i.e. ["line1 ", ...])
312+-- 'Char's representing white space.
313+wordsWithWhitespace :: Text -> [Text]
314+wordsWithWhitespace t@(Text arr off len) = loop 0 0 False
315+ where
316+ loop !start !n !wasSpace
317+ | n >= len = [Text arr (start+off) (n-start) | not (start == n)]
318+ | isSpace c = loop start (n+d) True
319+ | wasSpace = Text arr (start+off) (n-start) : loop n n False
320+ | otherwise = loop start (n+d) False
321+ where Iter c d = iter t n
322+{-# INLINE wordsWithWhitespace #-}
323+
324+-- | Split words into logical lines, 'True' in the tuple indicates line ends with a whitespace character that got deleted
325+splitWordsAtDisplayWidth :: Int -> [Text] -> [(Text, Bool)]
326+splitWordsAtDisplayWidth maxWidth wwws = reverse $ loop wwws 0 [] where
327+ appendOut :: [(Text,Bool)] -> Text -> Bool -> [(Text,Bool)]
328+ appendOut [] t b = [(t,b)]
329+ appendOut ((t',_):ts') t b = (t'<>t,b) : ts'
330+
331+ -- remove the last whitespace in output
332+ modifyOutForNewLine :: [(Text,Bool)] -> [(Text,Bool)]
333+ modifyOutForNewLine [] = error "should never happen"
334+ modifyOutForNewLine ((t',_):ts) = case T.unsnoc t' of
335+ Nothing -> error "should never happen"
336+ Just (t,lastChar) -> assert (isSpace lastChar) $ (t,True):ts -- assume last char is whitespace
337+
338+ loop :: [Text] -> Int -> [(Text,Bool)] -> [(Text,Bool)]
339+ loop [] _ out = out
340+ loop (x:xs) cumw out = r where
341+ newWidth = textWidth x + cumw
342+ r = if newWidth > maxWidth
343+ then if isSpace $ T.index x (toLogicalIndex (maxWidth - cumw) x)
344+ -- if line runs over but character of splitting is whitespace then split on the whitespace
345+ then let (t1,t2) = splitAtWidth (maxWidth - cumw) x
346+ in loop (T.drop 1 t2:xs) 0 [] <> appendOut out t1 True
347+ else if cumw == 0
348+ -- single word exceeds max width, so just split on the word
349+ then let (t1,t2) = splitAtWidth (maxWidth - cumw) x
350+ in loop (t2:xs) 0 [] <> appendOut out t1 False
351+ -- otherwise start a new line
352+ else loop (x:xs) 0 [] <> modifyOutForNewLine out
353+ else loop xs newWidth $ appendOut out x False
354+
355+
356+
357+-- | Wraps a logical line of text to fit within the given width. The first
358+-- wrapped line is offset by the number of columns provided. Subsequent wrapped
359+-- lines are not.
360+wrapWithOffsetAndAlignment
361+ :: TextAlignment
362+ -> Int -- ^ Maximum width
363+ -> Int -- ^ Offset for first line
364+ -> Text -- ^ Text to be wrapped
365+ -> [WrappedLine] -- (words on that line, hidden space char, offset from beginning of line)
366+wrapWithOffsetAndAlignment _ maxWidth _ _ | maxWidth <= 0 = []
367+wrapWithOffsetAndAlignment alignment maxWidth n txt = assert (n <= maxWidth) r where
368+ -- we pad by offset amount with any non-space character which we will remove later so that no changes need to be made to splitWordsAtDisplayWidth
369+ r' = splitWordsAtDisplayWidth maxWidth $ wordsWithWhitespace ( T.replicate n "." <> txt)
370+ fmapfn (t,b) = case alignment of
371+ TextAlignment_Left -> WrappedLine t b 0
372+ TextAlignment_Right -> WrappedLine t b (maxWidth-l)
373+ TextAlignment_Center -> WrappedLine t b ((maxWidth-l) `div` 2)
374+ where l = textWidth t
375+ r'' = case r' of
376+ [] -> []
377+ (x,b):xs -> (T.drop n x,b):xs
378+ r = fmap fmapfn r''
379+
380+-- converts deleted eol spaces into logical lines
381+eolSpacesToLogicalLines :: [[WrappedLine]] -> [[(Text, Int)]]
382+eolSpacesToLogicalLines = fmap (fmap (\(WrappedLine a _ c) -> (a,c))) . concatMap (L.groupBy (\(WrappedLine _ b _) _ -> not b))
383+
384+offsetMapWithAlignmentInternal :: [[WrappedLine]] -> OffsetMapWithAlignment
385+offsetMapWithAlignmentInternal = offsetMapWithAlignment . eolSpacesToLogicalLines
386+
387+offsetMapWithAlignment
388+ :: [[(Text, Int)]] -- ^ The outer list represents logical lines, inner list represents wrapped lines
389+ -> OffsetMapWithAlignment
390+offsetMapWithAlignment ts = evalState (offsetMap' ts) (0, 0)
391+ where
392+ offsetMap' xs = fmap Map.unions $ forM xs $ \x -> do
393+ maps <- forM x $ \(line,align) -> do
394+ let l = T.length line
395+ (dl, o) <- get
396+ put (dl + 1, o + l)
397+ return $ Map.singleton dl (align, o)
398+ (dl, o) <- get
399+ put (dl, o + 1)
400+ -- add additional offset to last line in wrapped lines (for newline char)
401+ return $ Map.adjust (\(align,_)->(align,o+1)) dl $ Map.unions maps
402+
403+
404+-- | Given a width and a 'TextZipper', produce a list of display lines
405+-- (i.e., lines of wrapped text) with special attributes applied to
406+-- certain segments (e.g., the cursor). Additionally, produce the current
407+-- y-coordinate of the cursor and a mapping from display line number to text
408+-- offset
409+displayLinesWithAlignment
410+ :: TextAlignment
411+ -> Int -- ^ Width, used for wrapping
412+ -> tag -- ^ Metadata for normal characters
413+ -> tag -- ^ Metadata for the cursor
414+ -> TextZipper -- ^ The text input contents and cursor state
415+ -> DisplayLines tag
416+displayLinesWithAlignment alignment width tag cursorTag (TextZipper lb b a la) =
417+ let linesBefore :: [[WrappedLine]] -- The wrapped lines before the cursor line
418+ linesBefore = map (wrapWithOffsetAndAlignment alignment width 0) $ reverse lb
419+ linesAfter :: [[WrappedLine]] -- The wrapped lines after the cursor line
420+ linesAfter = map (wrapWithOffsetAndAlignment alignment width 0) la
421+
422+ -- simulate trailing cursor character when computing OffsetMap
423+ afterWithCursor = if T.null a then " " else a
424+ offsets :: OffsetMapWithAlignment
425+ offsets = offsetMapWithAlignmentInternal $ mconcat
426+ [ linesBefore
427+ , [wrapWithOffsetAndAlignment alignment width 0 $ b <> afterWithCursor]
428+ , linesAfter
429+ ]
430+ flattenLines = concatMap (fmap _wrappedLines_text)
431+ spansBefore = map ((:[]) . Span tag) $ flattenLines linesBefore
432+ spansAfter = map ((:[]) . Span tag) $ flattenLines linesAfter
433+ -- Separate the spans before the cursor into
434+ -- * spans that are on earlier display lines (though on the same logical line), and
435+ -- * spans that are on the same display line
436+
437+ (spansCurrentBefore, spansCurLineBefore) = fromMaybe ([], []) $
438+ initLast $ map ((:[]) . Span tag) $ _wrappedLines_text <$> (wrapWithOffsetAndAlignment alignment width 0 b)
439+ -- Calculate the number of columns on the cursor's display line before the cursor
440+ curLineOffset = spansWidth spansCurLineBefore
441+ -- Check whether the spans on the current display line are long enough that
442+ -- the cursor has to go to the next line
443+ cursorAfterEOL = curLineOffset == width
444+ cursorCharWidth = case T.uncons a of
445+ Nothing -> 1
446+ Just (c, _) -> charWidth c
447+
448+ -- Separate the span after the cursor into
449+ -- * spans that are on the same display line, and
450+ -- * spans that are on later display lines (though on the same logical line)
451+
452+ (spansCurLineAfter, spansCurrentAfter) = fromMaybe ([], []) $
453+ headTail $ case T.uncons a of
454+ Nothing -> [[Span cursorTag " "]]
455+ Just (c, rest) ->
456+ let o = if cursorAfterEOL then cursorCharWidth else curLineOffset + cursorCharWidth
457+ cursor = Span cursorTag (T.singleton c)
458+ in case map ((:[]) . Span tag) $ _wrappedLines_text <$> (wrapWithOffsetAndAlignment alignment width o rest) of
459+ [] -> [[cursor]]
460+ (l:ls) -> (cursor : l) : ls
461+
462+ curLineSpanNormalCase = if cursorAfterEOL
463+ then [ spansCurLineBefore, spansCurLineAfter ]
464+ else [ spansCurLineBefore <> spansCurLineAfter ]
465+
466+ -- for right alignment, we want draw the cursor tag to be on the character just before the logical cursor position
467+ curLineSpan = if alignment == TextAlignment_Right && not cursorAfterEOL
468+ then case reverse spansCurLineBefore of
469+ [] -> curLineSpanNormalCase
470+ (Span _ x):xs -> case spansCurLineAfter of
471+ [] -> error "should not be possible" -- curLineSpanNormalCase
472+ (Span _ y):ys -> [reverse (Span cursorTag x:xs) <> ((Span tag y):ys)]
473+ else curLineSpanNormalCase
474+
475+ cursorY = sum
476+ [ length spansBefore
477+ , length spansCurrentBefore
478+ , if cursorAfterEOL then 1 else 0
479+ ]
480+ -- a little silly to convert back to text but whatever, it works
481+ cursorX = if cursorAfterEOL then 0 else textWidth (mconcat $ fmap (\(Span _ t) -> t) spansCurLineBefore)
482+
483+ in DisplayLines
484+ { _displayLines_spans = concat
485+ [ spansBefore
486+ , spansCurrentBefore
487+ , curLineSpan
488+ , spansCurrentAfter
489+ , spansAfter
490+ ]
491+ , _displayLines_offsetMap = offsets
492+ , _displayLines_cursorPos = (cursorX, cursorY)
493+ }
494+ where
495+ initLast :: [a] -> Maybe ([a], a)
496+ initLast = \case
497+ [] -> Nothing
498+ (x:xs) -> case initLast xs of
499+ Nothing -> Just ([], x)
500+ Just (ys, y) -> Just (x:ys, y)
501+ headTail :: [a] -> Maybe (a, [a])
502+ headTail = \case
503+ [] -> Nothing
504+ x:xs -> Just (x, xs)
505+
506+
507+-- | Move the cursor of the given 'TextZipper' to the logical position indicated
508+-- by the given display line coordinates, using the provided 'DisplayLinesWithAlignment'
509+-- information. If the x coordinate is beyond the end of a line, the cursor is
510+-- moved to the end of the line.
511+goToDisplayLinePosition :: Int -> Int -> DisplayLines tag -> TextZipper -> TextZipper
512+goToDisplayLinePosition x y dl tz =
513+ let offset = Map.lookup y $ _displayLines_offsetMap dl
514+ in case offset of
515+ Nothing -> tz
516+ Just (alignOff,o) ->
517+ let
518+ trueX = max 0 (x - alignOff)
519+ moveRight = case drop y $ _displayLines_spans dl of
520+ [] -> 0
521+ (s:_) -> charIndexAt trueX . stream . mconcat . fmap (\(Span _ t) -> t) $ s
522+ in rightN (o + moveRight) $ top tz
523+
524+-- | Given a width and a 'TextZipper', produce a list of display lines
525+-- (i.e., lines of wrapped text) with special attributes applied to
526+-- certain segments (e.g., the cursor). Additionally, produce the current
527+-- y-coordinate of the cursor and a mapping from display line number to text
528+-- offset
529+displayLines
530+ :: Int -- ^ Width, used for wrapping
531+ -> tag -- ^ Metadata for normal characters
532+ -> tag -- ^ Metadata for the cursor
533+ -> TextZipper -- ^ The text input contents and cursor state
534+ -> DisplayLines tag
535+displayLines = displayLinesWithAlignment TextAlignment_Left
536+
537+-- | Wraps a logical line of text to fit within the given width. The first
538+-- wrapped line is offset by the number of columns provided. Subsequent wrapped
539+-- lines are not.
540+wrapWithOffset
541+ :: Int -- ^ Maximum width
542+ -> Int -- ^ Offset for first line
543+ -> Text -- ^ Text to be wrapped
544+ -> [Text]
545+wrapWithOffset maxWidth n xs = _wrappedLines_text <$> wrapWithOffsetAndAlignment TextAlignment_Left maxWidth n xs
src/Reflex/Class/Switchable.hsdeleted−27
@@ -1,27 +0,0 @@
1−{-|
2−Module: Reflex.Class.Switchable
3−Description: A class for things that can be switched on the firing of an event
4−-}
5−{-# Language UndecidableInstances #-}
6−module Reflex.Class.Switchable where
7−
8−import Control.Monad
9−import Reflex
10−
11−-- | Class representing things that can be switched when the provided event occurs
12−class Reflex t => Switchable t w | w -> t where
13− switching :: MonadHold t m => w -> Event t w -> m w
14−
15−instance Reflex t => Switchable t (Event t a) where
16− switching = switchHold
17−
18−instance Reflex t => Switchable t (Dynamic t a) where
19− switching a e = fmap join $ holdDyn a e
20−
21−instance Reflex t => Switchable t (Behavior t a) where
22− switching = switcher
23−
24−instance (Reflex t, Switchable t a, Switchable t b) => Switchable t (a, b) where
25− switching (a, b) e = (,)
26− <$> switching a (fmap fst e)
27− <*> switching b (fmap snd e)
src/Reflex/Vty/Widget.hs+97−14
@@ -35,6 +35,7 @@ mainWidgetWithHandle
3535 , HasDisplayRegion t m
3636 , HasFocusReader t m
3737 , HasInput t m
38+ , HasTheme t m
3839 ) => m (Event t ()))
3940 -> IO ()
4041mainWidgetWithHandle vty child =
@@ -45,12 +46,14 @@ mainWidgetWithHandle vty child =
4546 let inp' = fforMaybe inp $ \case
4647 V.EvResize {} -> Nothing
4748 x -> Just x
48− (shutdown, images) <- runFocusReader (pure True) $ runDisplayRegion (fmap (\(w, h) -> Region 0 0 w h) size) $
49− runImageWriter $
50− runNodeIdT $
51− runInput inp' $ do
52− tellImages . ffor (current size) $ \(w, h) -> [V.charFill V.defAttr ' ' w h]
53− child
49+ (shutdown, images) <- runThemeReader (constant V.defAttr) $
50+ runFocusReader (pure True) $
51+ runDisplayRegion (fmap (\(w, h) -> Region 0 0 w h) size) $
52+ runImageWriter $
53+ runNodeIdT $
54+ runInput inp' $ do
55+ tellImages . ffor (current size) $ \(w, h) -> [V.charFill V.defAttr ' ' w h]
56+ child
5457 return $ VtyResult
5558 { _vtyResult_picture = fmap (V.picForLayers . reverse) images
5659 , _vtyResult_shutdown = shutdown
@@ -69,6 +72,7 @@ mainWidget
6972 , MonadNodeId m
7073 , HasDisplayRegion t m
7174 , HasFocusReader t m
75+ , HasTheme t m
7276 , HasInput t m
7377 ) => m (Event t ()))
7478 -> IO ()
@@ -206,8 +210,12 @@ inputInFocusedRegion
206210inputInFocusedRegion = do
207211 inp <- input
208212 reg <- current <$> askRegion
209− f <- current <$> focus
210− pure $ gate f $ fmapMaybe id $ attachWith mouseInRegion reg inp
213+ foc <- current <$> focus
214+ pure $ fmapMaybe id $ attachWith filterInput ((,) <$> reg <*> foc) inp
215+ where
216+ filterInput (r, f) = \case
217+ V.EvKey {} | not f -> Nothing
218+ x -> mouseInRegion r x
211219
212220-- * Getting and setting the display region
213221
@@ -220,14 +228,18 @@ data Region = Region
220228 }
221229 deriving (Show, Read, Eq, Ord)
222230
231+-- | A region that occupies no space.
232+nilRegion :: Region
233+nilRegion = Region 0 0 0 0
234+
223235-- | The width and height of a 'Region'
224236regionSize :: Region -> (Int, Int)
225237regionSize (Region _ _ w h) = (w, h)
226238
227239-- | 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
240+regionBlankImage :: V.Attr -> Region -> Image
241+regionBlankImage attr r@(Region _ _ width height) =
242+ withinImage r $ V.charFill attr ' ' width height
231243
232244-- | A class for things that know their own display size dimensions
233245class (Reflex t, Monad m) => HasDisplayRegion t m | m -> t where
@@ -395,7 +407,7 @@ newtype ImageWriter t m a = ImageWriter
395407 , NotReady t
396408 , PerformEvent t
397409 , PostBuild t
398− , TriggerEvent t
410+ , TriggerEvent t
399411 )
400412
401413instance MonadTrans (ImageWriter t) where
@@ -433,6 +445,75 @@ runImageWriter
433445 -> m (a, Behavior t [Image])
434446runImageWriter = runBehaviorWriterT . unImageWriter
435447
448+-- * Theming
449+
450+-- | A class for things that can be visually styled
451+class (Reflex t, Monad m) => HasTheme t m | m -> t where
452+ theme :: m (Behavior t V.Attr)
453+ default theme :: (f m' ~ m, Monad m', MonadTrans f, HasTheme t m') => m (Behavior t V.Attr)
454+ theme = lift theme
455+ localTheme :: (Behavior t V.Attr -> Behavior t V.Attr) -> m a -> m a
456+ default localTheme :: (f m' ~ m, Monad m', MFunctor f, HasTheme t m') => (Behavior t V.Attr -> Behavior t V.Attr) -> m a -> m a
457+ localTheme f = hoist (localTheme f)
458+
459+instance HasTheme t m => HasTheme t (ReaderT x m)
460+instance HasTheme t m => HasTheme t (BehaviorWriterT t x m)
461+instance HasTheme t m => HasTheme t (DynamicWriterT t x m)
462+instance HasTheme t m => HasTheme t (EventWriterT t x m)
463+instance HasTheme t m => HasTheme t (NodeIdT m)
464+instance HasTheme t m => HasTheme t (Input t m)
465+instance HasTheme t m => HasTheme t (ImageWriter t m)
466+instance HasTheme t m => HasTheme t (DisplayRegion t m)
467+instance HasTheme t m => HasTheme t (FocusReader t m)
468+
469+-- | A widget that has access to theme information
470+newtype ThemeReader t m a = ThemeReader
471+ { unThemeReader :: ReaderT (Behavior t V.Attr) m a }
472+ deriving
473+ ( Functor
474+ , Applicative
475+ , Monad
476+ , MonadFix
477+ , MonadHold t
478+ , MonadIO
479+ , MonadRef
480+ , MonadSample t
481+ )
482+
483+instance (Monad m, Reflex t) => HasTheme t (ThemeReader t m) where
484+ theme = ThemeReader ask
485+ localTheme f = ThemeReader . local f . unThemeReader
486+
487+deriving instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (ThemeReader t m)
488+deriving instance NotReady t m => NotReady t (ThemeReader t m)
489+deriving instance PerformEvent t m => PerformEvent t (ThemeReader t m)
490+deriving instance PostBuild t m => PostBuild t (ThemeReader t m)
491+deriving instance TriggerEvent t m => TriggerEvent t (ThemeReader t m)
492+instance HasImageWriter t m => HasImageWriter t (ThemeReader t m)
493+
494+instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (ThemeReader t m) where
495+ runWithReplace (ThemeReader a) e = ThemeReader $ runWithReplace a $ fmap unThemeReader e
496+ traverseIntMapWithKeyWithAdjust f m e = ThemeReader $ traverseIntMapWithKeyWithAdjust (\k v -> unThemeReader $ f k v) m e
497+ traverseDMapWithKeyWithAdjust f m e = ThemeReader $ traverseDMapWithKeyWithAdjust (\k v -> unThemeReader $ f k v) m e
498+ traverseDMapWithKeyWithAdjustWithMove f m e = ThemeReader $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unThemeReader $ f k v) m e
499+
500+instance MonadTrans (ThemeReader t) where
501+ lift = ThemeReader . lift
502+
503+instance MFunctor (ThemeReader t) where
504+ hoist f = ThemeReader . hoist f . unThemeReader
505+
506+instance MonadNodeId m => MonadNodeId (ThemeReader t m)
507+
508+-- | Run a 'ThemeReader' action with the given focus value
509+runThemeReader
510+ :: (Reflex t, Monad m)
511+ => Behavior t V.Attr
512+ -> ThemeReader t m a
513+ -> m a
514+runThemeReader b = flip runReaderT b . unThemeReader
515+
516+
436517-- ** Manipulating images
437518
438519-- | Translates and crops an 'Image' so that it is contained by
@@ -455,7 +536,7 @@ imagesInRegion reg = liftA2 (\r is -> map (withinImage r) is) reg
455536
456537-- * Running sub-widgets
457538
458−-- | Low-level widget combinator that runs a child 'Input' within
539+-- | Low-level widget combinator that runs a child widget within
459540-- a given region and context. This widget filters and modifies the input
460541-- that the child widget receives such that:
461542-- * unfocused widgets receive no key events
@@ -463,7 +544,7 @@ imagesInRegion reg = liftA2 (\r is -> map (withinImage r) is) reg
463544-- * mouse inputs inside the region have their coordinates translated such
464545-- that (0,0) is the top-left corner of the region
465546pane
466− :: (Reflex t, Monad m, MonadNodeId m, HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
547+ :: (Reflex t, Monad m, HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
467548 => Dynamic t Region
468549 -> Dynamic t Bool -- ^ Whether the widget should be focused when the parent is.
469550 -> m a
@@ -473,6 +554,8 @@ pane dr foc child = localRegion (const dr) $
473554 localFocus (const foc) $
474555 inputInFocusedRegion >>= \e -> localInput (const e) child
475556
557+-- * Misc
558+
476559-- | A widget that draws nothing
477560blank :: Monad m => m ()
478561blank = return ()
src/Reflex/Vty/Widget/Box.hs+19−17
@@ -3,7 +3,6 @@
33-}
44module Reflex.Vty.Widget.Box where
55
6−import Control.Monad.NodeId
76import Data.Default
87import Data.Text (Text)
98import qualified Data.Text as T
@@ -14,7 +13,7 @@ import Reflex.Vty.Widget
1413import Reflex.Vty.Widget.Text
1514
1615-- | Fill the background with the bottom box style
17−hRule :: (HasDisplayRegion t m, HasImageWriter t m) => BoxStyle -> m ()
16+hRule :: (HasDisplayRegion t m, HasImageWriter t m, HasTheme t m) => BoxStyle -> m ()
1817hRule boxStyle = fill $ pure (_boxStyle_s boxStyle)
1918
2019-- | Defines a set of symbols to use to draw the outlines of boxes
@@ -55,7 +54,7 @@ roundedBoxStyle :: BoxStyle
5554roundedBoxStyle = BoxStyle '╭' '─' '╮' '│' '╯' '─' '╰' '│'
5655
5756-- | 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)
57+boxTitle :: (Monad m, Reflex t ,HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m, HasTheme t m)
5958 => Behavior t BoxStyle
6059 -> Behavior t Text
6160 -> m a
@@ -63,36 +62,39 @@ boxTitle :: (Monad m, Reflex t, MonadNodeId m, HasDisplayRegion t m, HasImageWri
6362boxTitle boxStyle title child = do
6463 dh <- displayHeight
6564 dw <- displayWidth
65+ bt <- theme
6666 let boxReg = Region 0 0 <$> dw <*> dh
6767 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))
68+
69+ tellImages (boxImages <$> bt <*> title <*> boxStyle <*> current boxReg)
70+ tellImages (ffor2 (current innerReg) bt (\r attr -> [regionBlankImage attr r]))
71+
7072 pane innerReg (pure True) child
7173 where
72− boxImages :: Text -> BoxStyle -> Region -> [Image]
73− boxImages title' style (Region left top width height) =
74+ boxImages :: V.Attr -> Text -> BoxStyle -> Region -> [Image]
75+ boxImages attr title' style (Region left top width height) =
7476 let right = left + width - 1
7577 bottom = top + height - 1
7678 sides =
7779 [ withinImage (Region (left + 1) top (width - 2) 1) $
78− V.text' V.defAttr $
80+ V.text' attr $
7981 hPadText title' (_boxStyle_n style) (width - 2)
8082 , withinImage (Region right (top + 1) 1 (height - 2)) $
81− V.charFill V.defAttr (_boxStyle_e style) 1 (height - 2)
83+ V.charFill attr (_boxStyle_e style) 1 (height - 2)
8284 , withinImage (Region (left + 1) bottom (width - 2) 1) $
83− V.charFill V.defAttr (_boxStyle_s style) (width - 2) 1
85+ V.charFill attr (_boxStyle_s style) (width - 2) 1
8486 , withinImage (Region left (top + 1) 1 (height - 2)) $
85− V.charFill V.defAttr (_boxStyle_w style) 1 (height - 2)
87+ V.charFill attr (_boxStyle_w style) 1 (height - 2)
8688 ]
8789 corners =
8890 [ withinImage (Region left top 1 1) $
89− V.char V.defAttr (_boxStyle_nw style)
91+ V.char attr (_boxStyle_nw style)
9092 , withinImage (Region right top 1 1) $
91− V.char V.defAttr (_boxStyle_ne style)
93+ V.char attr (_boxStyle_ne style)
9294 , withinImage (Region right bottom 1 1) $
93− V.char V.defAttr (_boxStyle_se style)
95+ V.char attr (_boxStyle_se style)
9496 , withinImage (Region left bottom 1 1) $
95− V.char V.defAttr (_boxStyle_sw style)
97+ V.char attr (_boxStyle_sw style)
9698 ]
9799 in sides ++ if width > 1 && height > 1 then corners else []
98100 hPadText :: T.Text -> Char -> Int -> T.Text
@@ -107,7 +109,7 @@ boxTitle boxStyle title child = do
107109 right = mkHalf delta
108110
109111-- | 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)
112+box :: (Monad m, Reflex t, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m, HasTheme t m)
111113 => Behavior t BoxStyle
112114 -> m a
113115 -> m a
@@ -115,7 +117,7 @@ box boxStyle = boxTitle boxStyle mempty
115117
116118-- | A box whose style is static
117119boxStatic
118− :: (Monad m, Reflex t, MonadNodeId m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m)
120+ :: (Monad m, Reflex t, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m, HasTheme t m)
119121 => BoxStyle
120122 -> m a
121123 -> m a
src/Reflex/Vty/Widget/Input.hs+9−8
@@ -13,7 +13,6 @@ import Reflex.Vty.Widget.Input.Text as Export
1313
1414import Control.Monad (join)
1515import Control.Monad.Fix (MonadFix)
16−import Control.Monad.NodeId (MonadNodeId)
1716import Data.Default (Default(..))
1817import Data.Text (Text)
1918import qualified Graphics.Vty as V
@@ -35,7 +34,7 @@ instance Reflex t => Default (ButtonConfig t) where
3534
3635-- | A button widget that contains a sub-widget
3736button
38− :: (Reflex t, Monad m, MonadNodeId m, HasFocusReader t m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m)
37+ :: (Reflex t, Monad m, HasFocusReader t m, HasTheme t m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m)
3938 => ButtonConfig t
4039 -> m ()
4140 -> m (Event t ())
@@ -53,7 +52,7 @@ button cfg child = do
5352
5453-- | A button widget that displays text that can change
5554textButton
56− :: (Reflex t, Monad m, MonadNodeId m, HasDisplayRegion t m, HasFocusReader t m, HasImageWriter t m, HasInput t m)
55+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasFocusReader t m, HasTheme t m, HasImageWriter t m, HasInput t m)
5756 => ButtonConfig t
5857 -> Behavior t Text
5958 -> m (Event t ())
@@ -61,7 +60,7 @@ textButton cfg = button cfg . text -- TODO Centering etc.
6160
6261-- | A button widget that displays a static bit of text
6362textButtonStatic
64− :: (Reflex t, Monad m, MonadNodeId m, HasDisplayRegion t m, HasFocusReader t m, HasImageWriter t m, HasInput t m)
63+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasFocusReader t m, HasTheme t m, HasImageWriter t m, HasInput t m)
6564 => ButtonConfig t
6665 -> Text
6766 -> m (Event t ())
@@ -71,19 +70,20 @@ textButtonStatic cfg = textButton cfg . pure
7170
7271-- | A clickable link widget
7372link
74− :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m)
73+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasTheme t m)
7574 => Behavior t Text
7675 -> m (Event t MouseUp)
7776link t = do
77+ bt <- theme
7878 let cfg = RichTextConfig
79− { _richTextConfig_attributes = pure $ V.withStyle V.defAttr V.underline
79+ { _richTextConfig_attributes = fmap (\attr -> V.withStyle attr V.underline) bt
8080 }
8181 richText cfg t
8282 mouseUp
8383
8484-- | A clickable link widget with a static label
8585linkStatic
86− :: (Reflex t, Monad m, HasImageWriter t m, HasDisplayRegion t m, HasInput t m)
86+ :: (Reflex t, Monad m, HasImageWriter t m, HasDisplayRegion t m, HasInput t m, HasTheme t m)
8787 => Text
8888 -> m (Event t MouseUp)
8989linkStatic = link . pure
@@ -116,6 +116,7 @@ checkboxStyleTick = CheckboxStyle
116116-- | Configuration options for a checkbox
117117data CheckboxConfig t = CheckboxConfig
118118 { _checkboxConfig_checkboxStyle :: Behavior t CheckboxStyle
119+ -- TODO DELETE and use HasTheme instead
119120 , _checkboxConfig_attributes :: Behavior t V.Attr
120121 , _checkboxConfig_setValue :: Event t Bool
121122 }
@@ -129,7 +130,7 @@ instance (Reflex t) => Default (CheckboxConfig t) where
129130
130131-- | A checkbox widget
131132checkbox
132− :: (MonadHold t m, MonadFix m, Reflex t, HasInput t m, HasDisplayRegion t m, HasImageWriter t m, HasFocusReader t m)
133+ :: (MonadHold t m, MonadFix m, Reflex t, HasInput t m, HasDisplayRegion t m, HasImageWriter t m, HasFocusReader t m, HasTheme t m)
133134 => CheckboxConfig t
134135 -> Bool
135136 -> m (Dynamic t Bool)
src/Reflex/Vty/Widget/Input/Text.hs+16−12
@@ -9,7 +9,6 @@ module Reflex.Vty.Widget.Input.Text
99
1010import Control.Monad (join)
1111import Control.Monad.Fix (MonadFix)
12−import Control.Monad.NodeId (MonadNodeId)
1312import Data.Default (Default(..))
1413import Data.Text (Text)
1514import Data.Text.Zipper
@@ -44,7 +43,7 @@ data TextInput t = TextInput
4443
4544-- | A widget that allows text input
4645textInput
47− :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m, HasFocusReader t m, HasDisplayRegion t m, HasImageWriter t m, HasDisplayRegion t m)
46+ :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m, HasFocusReader t m, HasTheme t m, HasDisplayRegion t m, HasImageWriter t m, HasDisplayRegion t m)
4847 => TextInputConfig t
4948 -> m (TextInput t)
5049textInput cfg = do
@@ -52,6 +51,8 @@ textInput cfg = do
5251 f <- focus
5352 dh <- displayHeight
5453 dw <- displayWidth
54+ bt <- theme
55+ attr0 <- sample bt
5556 rec v <- foldDyn ($) (_textInputConfig_initialValue cfg) $ mergeWith (.)
5657 [ uncurry (updateTextZipper (_textInputConfig_tabWidth cfg)) <$> attach (current dh) i
5758 , _textInputConfig_modify cfg
@@ -60,13 +61,20 @@ textInput cfg = do
6061 goToDisplayLinePosition mx (st + my) dl
6162 ]
6263 click <- mouseDown V.BLeft
63− let cursorAttrs = ffor f $ \x -> if x then cursorAttributes else V.defAttr
64− let rows = (\w s c -> displayLines w V.defAttr c s)
64+
65+ -- TODO reverseVideo is prob not what we want. Does not work with `darkTheme` in example.hs (cursor is dark rather than light bg)
66+ let toCursorAttrs attr = V.withStyle attr V.reverseVideo
67+ rowInputDyn = (,,)
6568 <$> dw
6669 <*> (mapZipper <$> _textInputConfig_display cfg <*> v)
67− <*> cursorAttrs
70+ <*> f
71+ toDisplayLines attr (w, s, x) =
72+ let c = if x then toCursorAttrs attr else attr
73+ in displayLines w attr c s
74+ attrDyn <- holdDyn attr0 $ pushAlways (\_ -> sample bt) (updated rowInputDyn)
75+ let rows = ffor2 attrDyn rowInputDyn toDisplayLines
6876 img = images . _displayLines_spans <$> rows
69− y <- holdUniqDyn $ _displayLines_cursorY <$> rows
77+ y <- holdUniqDyn $ fmap snd _displayLines_cursorPos <$> rows
7078 let newScrollTop :: Int -> (Int, Int) -> Int
7179 newScrollTop st (h, cursorY)
7280 | cursorY < st = cursorY
@@ -82,7 +90,7 @@ textInput cfg = do
8290
8391-- | A widget that allows multiline text input
8492multilineTextInput
85− :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m, HasFocusReader t m, HasDisplayRegion t m, HasImageWriter t m)
93+ :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m, HasFocusReader t m, HasTheme t m, HasDisplayRegion t m, HasImageWriter t m)
8694 => TextInputConfig t
8795 -> m (TextInput t)
8896multilineTextInput cfg = do
@@ -100,7 +108,7 @@ multilineTextInput cfg = do
100108-- the computed line count to greedily size the tile when vertically
101109-- oriented, and uses the fallback width when horizontally oriented.
102110textInputTile
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)
111+ :: (Monad m, Reflex t, MonadFix m, HasLayout t m, HasInput t m, HasFocus t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m, HasTheme t m)
104112 => m (TextInput t)
105113 -> Dynamic t Int
106114 -> m (TextInput t)
@@ -112,10 +120,6 @@ textInputTile txt width = do
112120 Orientation_Row -> width
113121 return t
114122
115−-- | Default attributes for the text cursor
116−cursorAttributes :: V.Attr
117−cursorAttributes = V.withStyle V.defAttr V.reverseVideo
118−
119123-- | Turn a set of display line rows into a list of images (one per line)
120124images :: [[Span V.Attr]] -> [V.Image]
121125images = map (V.horizCat . map spanToImage)
src/Reflex/Vty/Widget/Layout.hs+14−9
@@ -133,6 +133,8 @@ instance (HasImageWriter t m, MonadFix m) => HasImageWriter t (Focus t m) where
133133
134134instance (HasFocusReader t m, Monad m) => HasFocusReader t (Focus t m)
135135
136+instance (HasTheme t m, Monad m) => HasTheme t (Focus t m)
137+
136138instance (Reflex t, MonadFix m, MonadNodeId m) => HasFocus t (Focus t m) where
137139 makeFocus = do
138140 fid <- FocusId <$> lift getNextNodeId
@@ -184,7 +186,7 @@ anyChildFocused f = do
184186
185187-- | Request focus be shifted backward and forward based on tab presses. <Tab>
186188-- shifts focus forward and <Shift+Tab> shifts focus backward.
187−tabNavigation :: (Reflex t, MonadNodeId m, HasInput t m, HasFocus t m) => m ()
189+tabNavigation :: (Reflex t, HasInput t m, HasFocus t m) => m ()
188190tabNavigation = do
189191 fwd <- fmap (const 1) <$> key (V.KChar '\t')
190192 back <- fmap (const (-1)) <$> key V.KBackTab
@@ -242,14 +244,14 @@ data Orientation = Orientation_Column
242244
243245-- | Create a row-oriented 'axis'
244246row
245− :: (Reflex t, MonadNodeId m, MonadFix m, HasLayout t m)
247+ :: (Reflex t, MonadFix m, HasLayout t m)
246248 => m a
247249 -> m a
248250row = axis (pure Orientation_Row) flex
249251
250252-- | Create a column-oriented 'axis'
251253col
252− :: (Reflex t, MonadNodeId m, MonadFix m, HasLayout t m)
254+ :: (Reflex t, MonadFix m, HasLayout t m)
253255 => m a
254256 -> m a
255257col = axis (pure Orientation_Column) flex
@@ -361,7 +363,7 @@ chunk o r (offset, sz) = case o of
361363
362364-- | A class of operations for creating screen layouts.
363365class Monad m => HasLayout t m | m -> t where
364− -- | Creates a parent element in the current layout with the given size
366+ -- | Starts a parent element in the current layout with the given size
365367 -- constraint, which lays out its children according to the provided
366368 -- orientation.
367369 axis :: Dynamic t Orientation -> Dynamic t Constraint -> m a -> m a
@@ -430,17 +432,20 @@ instance (HasDisplayRegion t m, HasImageWriter t m, MonadFix m) => HasImageWrite
430432
431433instance (HasFocusReader t m, Monad m) => HasFocusReader t (Layout t m)
432434
435+instance (HasTheme t m, Monad m) => HasTheme t (Layout t m)
436+
433437instance (Monad m, MonadNodeId m, Reflex t, MonadFix m) => HasLayout t (Layout t m) where
434438 axis o c (Layout x) = Layout $ do
435439 nodeId <- getNextNodeId
436− (result, forest) <- lift $ local (\t -> fromMaybe (LayoutTree (Region 0 0 0 0, Orientation_Column) mempty) . lookupLF nodeId . childrenLT <$> t) $ runDynamicWriterT x
440+ let dummyParentLayout = LayoutTree (nilRegion, Orientation_Column) mempty
441+ (result, forest) <- lift $ local (\t -> fromMaybe dummyParentLayout . lookupLF nodeId . childrenLT <$> t) $ runDynamicWriterT x
437442 tellDyn $ singletonLF nodeId <$> (LayoutTree <$> ((,) <$> c <*> o) <*> forest)
438443 pure result
439444 region c = do
440445 nodeId <- lift getNextNodeId
441446 Layout $ tellDyn $ ffor c $ \c' -> singletonLF nodeId $ LayoutTree (c', Orientation_Row) mempty
442447 solutions <- Layout ask
443− pure $ maybe (Region 0 0 0 0) (fst . rootLT) . lookupLF nodeId . childrenLT <$> solutions
448+ pure $ maybe nilRegion (fst . rootLT) . lookupLF nodeId . childrenLT <$> solutions
444449 askOrientation = Layout $ asks $ fmap (snd . rootLT)
445450
446451instance (MonadFix m, HasFocus t m) => HasFocus t (Layout t m) where
@@ -505,7 +510,7 @@ initManager_ = fmap fst . initManager
505510-- provided constraint. Returns the 'FocusId' allowing for manual focus
506511-- management.
507512tile'
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)
513+ :: (MonadFix m, Reflex t, HasInput t m, HasFocus t m, HasLayout t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
509514 => Dynamic t Constraint
510515 -> m a
511516 -> m (FocusId, a)
@@ -524,7 +529,7 @@ tile' c w = do
524529-- | A widget that is focusable and occupies a layout region based on the
525530-- provided constraint.
526531tile
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)
532+ :: (MonadFix m, Reflex t, HasInput t m, HasFocus t m, HasLayout t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
528533 => Dynamic t Constraint
529534 -> m a
530535 -> m a
@@ -535,7 +540,7 @@ tile c = fmap snd . tile' c
535540-- | A widget that is not focusable and occupies a layout region based on the
536541-- provided constraint.
537542grout
538− :: (Reflex t, MonadNodeId m, HasLayout t m, HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
543+ :: (Reflex t, HasLayout t m, HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
539544 => Dynamic t Constraint
540545 -> m a
541546 -> m a
src/Reflex/Vty/Widget/Split.hs+3−4
@@ -5,7 +5,6 @@ module Reflex.Vty.Widget.Split where
55
66import Control.Applicative
77import Control.Monad.Fix
8−import Control.Monad.NodeId
98import Graphics.Vty as V
109import Reflex
1110import Reflex.Vty.Widget
@@ -14,7 +13,7 @@ import Reflex.Vty.Widget.Input.Mouse
1413-- | A split of the available space into two parts with a draggable separator.
1514-- Starts with half the space allocated to each, and the first pane has focus.
1615-- 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)
16+splitVDrag :: (Reflex t, MonadFix m, MonadHold t m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m)
1817 => m ()
1918 -> m a
2019 -> m b
@@ -53,7 +52,7 @@ splitVDrag wS wA wB = do
5352
5453-- | A plain split of the available space into vertically stacked panes.
5554-- 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)
55+splitV :: (Reflex t, Monad m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m)
5756 => Dynamic t (Int -> Int)
5857 -- ^ Function used to determine size of first pane based on available size
5958 -> Dynamic t (Bool, Bool)
@@ -74,7 +73,7 @@ splitV sizeFunD focD wA wB = do
7473
7574-- | A plain split of the available space into horizontally stacked panes.
7675-- 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)
76+splitH :: (Reflex t, Monad m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m)
7877 => Dynamic t (Int -> Int)
7978 -- ^ Function used to determine size of first pane based on available size
8079 -> Dynamic t (Bool, Bool)
src/Reflex/Vty/Widget/Text.hs+17−10
@@ -14,13 +14,15 @@ import Reflex.Vty.Widget
1414import Reflex.Vty.Widget.Input.Mouse
1515
1616-- | Fill the background with a particular character.
17−fill :: (HasDisplayRegion t m, HasImageWriter t m) => Behavior t Char -> m ()
17+fill :: (HasDisplayRegion t m, HasImageWriter t m, HasTheme t m) => Behavior t Char -> m ()
1818fill bc = do
1919 dw <- displayWidth
2020 dh <- displayHeight
21+ bt <- theme
2122 let fillImg =
22− (\w h c -> [V.charFill V.defAttr c w h])
23− <$> current dw
23+ (\attr w h c -> [V.charFill attr c w h])
24+ <$> bt
25+ <*> current dw
2426 <*> current dh
2527 <*> bc
2628 tellImages fillImg
@@ -33,9 +35,11 @@ data RichTextConfig t = RichTextConfig
3335instance Reflex t => Default (RichTextConfig t) where
3436 def = RichTextConfig $ pure V.defAttr
3537
38+
39+-- TODO delete this and use new local theming
3640-- | A widget that displays text with custom time-varying attributes
3741richText
38− :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m)
42+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m, HasTheme t m)
3943 => RichTextConfig t
4044 -> Behavior t Text
4145 -> m ()
@@ -53,15 +57,17 @@ richText cfg t = do
5357
5458-- | Renders text, wrapped to the container width
5559text
56− :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m)
60+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m, HasTheme t m)
5761 => Behavior t Text
5862 -> m ()
59−text = richText def
63+text t = do
64+ bt <- theme
65+ richText (RichTextConfig bt) t
6066
6167-- | Scrollable text widget. The output pair exposes the current scroll position and total number of lines (including those
6268-- that are hidden)
6369scrollableText
64− :: forall t m. (Reflex t, MonadHold t m, MonadFix m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m)
70+ :: forall t m. (Reflex t, MonadHold t m, MonadFix m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasTheme t m)
6571 => Event t Int
6672 -- ^ Number of lines to scroll by
6773 -> Behavior t Text
@@ -69,7 +75,8 @@ scrollableText
6975 -- ^ (Current scroll position, total number of lines)
7076scrollableText scrollBy t = do
7177 dw <- displayWidth
72− let imgs = wrap <$> current dw <*> t
78+ bt <- theme
79+ let imgs = wrap <$> bt <*> current dw <*> t
7380 kup <- key V.KUp
7481 kdown <- key V.KDown
7582 m <- mouseScroll
@@ -88,12 +95,12 @@ scrollableText scrollBy t = do
8895 tellImages $ fmap ((:[]) . V.vertCat) $ drop <$> current lineIndex <*> imgs
8996 return $ (,) <$> ((+) <$> current lineIndex <*> pure 1) <*> (length <$> imgs)
9097 where
91− wrap maxWidth = concatMap (fmap (V.string V.defAttr . T.unpack) . TZ.wrapWithOffset maxWidth 0) . T.split (=='\n')
98+ wrap attr maxWidth = concatMap (fmap (V.string attr . T.unpack) . TZ.wrapWithOffset maxWidth 0) . T.split (=='\n')
9299
93100-- | Renders any behavior whose value can be converted to
94101-- 'String' as text
95102display
96− :: (Reflex t, Monad m, Show a, HasDisplayRegion t m, HasImageWriter t m)
103+ :: (Reflex t, Monad m, Show a, HasDisplayRegion t m, HasImageWriter t m, HasTheme t m)
97104 => Behavior t a
98105 -> m ()
99106display a = text $ T.pack . show <$> a
test/Data/Text/ZipperSpec.hsnew file+104
@@ -0,0 +1,104 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Data.Text.ZipperSpec(
4+ spec
5+) where
6+
7+import Prelude
8+
9+import Test.Hspec
10+
11+import qualified Data.Map as Map
12+import qualified Data.Text as T
13+import Control.Monad
14+
15+import Data.Text.Zipper
16+
17+
18+someSentence = "12345 1234 12"
19+
20+splitSentenceAtDisplayWidth :: Int -> T.Text -> [(T.Text, Bool)]
21+splitSentenceAtDisplayWidth w t = splitWordsAtDisplayWidth w (wordsWithWhitespace t)
22+
23+spec :: Spec
24+spec =
25+ describe "Zipper" $ do
26+ it "wordsWithWhitespace" $ do
27+ wordsWithWhitespace "" `shouldBe` []
28+ wordsWithWhitespace "😱😱😱" `shouldBe` ["😱😱😱"]
29+ wordsWithWhitespace "abcd efgf f" `shouldBe` ["abcd ","efgf ", "f"]
30+ wordsWithWhitespace "aoeu " `shouldBe` ["aoeu "]
31+ it "splitWordsAtDisplayWidth" $ do
32+ fmap fst (splitSentenceAtDisplayWidth 5 "123456") `shouldBe` ["12345","6"]
33+ fmap fst (splitSentenceAtDisplayWidth 5 "12345 6") `shouldBe` ["12345","6"]
34+ fmap fst (splitSentenceAtDisplayWidth 5 "1234 56") `shouldBe` ["1234","56"]
35+ fmap fst (splitSentenceAtDisplayWidth 5 "12345678912345") `shouldBe` ["12345","67891","2345"]
36+ fmap fst (splitSentenceAtDisplayWidth 5 "1234 56") `shouldBe` ["1234 "," 56"]
37+ fmap fst (splitSentenceAtDisplayWidth 8 "1 2 3 4 5 6 7 8 9 1") `shouldBe` ["1 2 3 4","5 6 7 8", "9 1"]
38+ it "wrapWithOffsetAndAlignment" $ do
39+ wrapWithOffsetAndAlignment TextAlignment_Left 5 0 someSentence `shouldBe` [(WrappedLine "12345" True 0), (WrappedLine "1234" True 0), (WrappedLine "12" False 0)]
40+ wrapWithOffsetAndAlignment TextAlignment_Right 5 0 someSentence `shouldBe` [(WrappedLine "12345" True 0), (WrappedLine "1234" True 1), (WrappedLine "12" False 3)]
41+ wrapWithOffsetAndAlignment TextAlignment_Center 5 0 someSentence `shouldBe` [(WrappedLine "12345" True 0), (WrappedLine "1234" True 0), (WrappedLine "12" False 1)]
42+ wrapWithOffsetAndAlignment TextAlignment_Left 5 1 someSentence `shouldBe` [(WrappedLine "1234" False 0), (WrappedLine "5" True 0), (WrappedLine "1234" True 0), (WrappedLine "12" False 0)]
43+
44+ -- leading spaces and offset case
45+ wrapWithOffsetAndAlignment TextAlignment_Left 5 1 (" " <> someSentence) `shouldBe` [(WrappedLine " " True 0), (WrappedLine "12345" True 0), (WrappedLine "1234" True 0), (WrappedLine "12" False 0)]
46+ it "eolSpacesToLogicalLines" $ do
47+ eolSpacesToLogicalLines
48+ [
49+ [ (WrappedLine "😱" True 1), (WrappedLine "😱" False 2), (WrappedLine "😱" False 3) ]
50+ , [ (WrappedLine "aa" True 1), (WrappedLine "aa" True 2), (WrappedLine "aa" False 3) ]
51+ ]
52+ `shouldBe`
53+ [
54+ [ ("😱",1) ]
55+ , [ ("😱",2), ("😱",3) ]
56+ , [ ("aa",1) ]
57+ , [ ("aa",2) ]
58+ , [ ("aa",3) ]
59+ ]
60+ it "offsetMapWithAlignmentInternal" $ do
61+ offsetMapWithAlignmentInternal
62+ [
63+ [ (WrappedLine "😱" True 1), (WrappedLine "😱" False 2), (WrappedLine "😱" False 3) ]
64+ , [ (WrappedLine "aa" True 1), (WrappedLine "aa" True 2), (WrappedLine "aa" False 3) ]
65+ ]
66+ `shouldBe`
67+ Map.fromList
68+ [ (0, (1,0))
69+ , (1, (2,2)) -- jump by 1 for char and 1 for space
70+ , (2, (3,3)) -- jump by 1 for char
71+ , (3, (1,5)) -- jump by 1 for char and 1 for newline
72+ , (4, (2,8)) -- jump by 2 for char and 1 for space
73+ , (5, (3,11)) -- jump by 2 for char and 1 for space
74+ ]
75+ it "displayLines - cursorPos" $ do
76+ let
77+ dl0 = displayLinesWithAlignment TextAlignment_Right 10 () () (fromText "")
78+ dl1 = displayLinesWithAlignment TextAlignment_Right 10 () () (fromText "aoeu")
79+ dl2 = displayLinesWithAlignment TextAlignment_Right 10 () () (fromText "aoeu\n")
80+ dl3 = displayLinesWithAlignment TextAlignment_Right 10 () () (fromText "0123456789")
81+ dl4 = displayLinesWithAlignment TextAlignment_Right 10 () () (insertChar 'a' $ fromText "aoeu")
82+ dl5 = displayLinesWithAlignment TextAlignment_Right 10 () () (left $ insertChar 'a' $ fromText "aoeu")
83+ dl6 = displayLinesWithAlignment TextAlignment_Right 10 () () (deleteLeft $ insertChar 'a' $ fromText "aoeu")
84+ _displayLines_cursorPos dl0 `shouldBe` (0,0)
85+ _displayLines_cursorPos dl1 `shouldBe` (4,0)
86+ _displayLines_cursorPos dl2 `shouldBe` (0,1)
87+ _displayLines_cursorPos dl3 `shouldBe` (0,1)
88+ _displayLines_cursorPos dl4 `shouldBe` (5,0)
89+ _displayLines_cursorPos dl5 `shouldBe` (4,0)
90+ _displayLines_cursorPos dl6 `shouldBe` (4,0)
91+ it "displayLines - offsetMap" $ do
92+ let
93+ dl0 = displayLinesWithAlignment TextAlignment_Left 5 () () (end $ fromText "aoeku")
94+ _displayLines_cursorPos dl0 `shouldBe` (0,1)
95+ Map.size (_displayLines_offsetMap dl0) `shouldBe` 2 -- cursor character is on second line
96+ it "displayLinesWithAlignment - spans" $ do
97+ let
98+ someText = top $ fromText "0123456789abcdefgh"
99+ -- outer span length should be invariant when changing TextAlignment and CursorPosition
100+ forM_ [0..4] $ \x -> do
101+ forM_ [TextAlignment_Left, TextAlignment_Center, TextAlignment_Right] $ \ta -> do
102+ let t = rightN x $ someText
103+ length (_displayLines_spans $ (displayLinesWithAlignment ta 5 () () t)) `shouldBe` 4
104+ length (_displayLines_spans $ (displayLinesWithAlignment ta 10 () () t)) `shouldBe` 2
test/Spec.hsnew file+15
@@ -0,0 +1,15 @@
1+-- hspec auto-discovery stuff
2+-- does not work with current CircleCI image so instead we do it manually for now
3+-- {-# OPTIONS_GHC -F -pgmF hspec-discover #-}
4+
5+
6+import Test.Hspec
7+
8+import qualified Data.Text.ZipperSpec
9+
10+main :: IO ()
11+main = hspec spec
12+
13+spec :: Spec
14+spec = do
15+ describe "Data.Text.ZipperSpec" Data.Text.ZipperSpec.spec