on cf82eec help sign in
16 files changed+378−642
since splitsnapshots
unifiedsplit
.github/workflows/haskell.yml+10−12 .travis.yml+112 ChangeLog.md+11−26 reflex-vty.cabal+7−25 src-bin/Example/CPU.hs+4−2 src-bin/example.hs+6−12 src/Data/Text/Zipper.hs+157−280 src/Reflex/Vty/Widget.hs+11−88 src/Reflex/Vty/Widget/Box.hs+17−19 src/Reflex/Vty/Widget/Input.hs+8−9 src/Reflex/Vty/Widget/Input/Text.hs+12−16 src/Reflex/Vty/Widget/Layout.hs+9−14 src/Reflex/Vty/Widget/Split.hs+4−3 src/Reflex/Vty/Widget/Text.hs+10−17 test/Data/Text/ZipperSpec.hs−104 test/Spec.hs−15
.github/workflows/haskell.yml+10−12
@@ -1,33 +1,33 @@
1−name: github-action
1+name: Haskell CI
22
33on: [push, pull_request]
44
55jobs:
66 build:
7+
8+ runs-on: ${{ matrix.os }}
79 strategy:
810 matrix:
9− ghc: ['8.4.4', '8.6.5', '8.8.4', '8.10.2']
11+ ghc: ['8.4.4', '8.6.5', '8.8.4']
1012 os: ['ubuntu-latest', 'macos-latest']
11− runs-on: ${{ matrix.os }}
12−
13− name: GHC ${{ matrix.ghc }} on ${{ matrix.os }}
1413 steps:
1514 - uses: actions/checkout@v2
1615 - uses: actions/setup-haskell@v1
1716 with:
1817 ghc-version: ${{ matrix.ghc }}
18+ cabal-version: '3.0'
19+
1920 - name: Cache
2021 uses: actions/cache@v1
2122 env:
2223 cache-name: cache-cabal
2324 with:
2425 path: ~/.cabal
25− key: ${{ runner.os }}-${{ matrix.ghc }}-build-${{ env.cache-name }}-${{ hashFiles('**/*.cabal') }}-${{ hashFiles('**/cabal.project') }}
26+ key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/*.cabal') }}-${{ hashFiles('**/cabal.project') }}
2627 restore-keys: |
27− ${{ runner.os }}-${{ matrix.ghc }}-build-${{ env.cache-name }}-
28− ${{ runner.os }}-${{ matrix.ghc }}-build-
29− ${{ runner.os }}-${{ matrix.ghc }}-
30− ${{ runner.os }}
28+ ${{ runner.os }}-build-${{ env.cache-name }}-
29+ ${{ runner.os }}-build-
30+ ${{ runner.os }}-
3131
3232 - name: Install dependencies
3333 run: |
@@ -37,5 +37,3 @@ 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.ymlnew file+112
@@ -0,0 +1,112 @@
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+11−26
@@ -33,8 +33,6 @@
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
3836 * Remove `DynRegion` and `currentRegion`. Use `Dynamic t Region` and `current` instead. This also changes the type of `pane`'s argument.
3937 * `CheckboxConfig` now has a field taking an `Event` to set the value of the checkbox.
4038 * `checkbox` now accepts keyboard input (spacebar to check and uncheck) and is displayed in bold when focused.
@@ -56,28 +54,21 @@
5654 * `splitH`: Now requires `HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m`
5755 * `splitVDrag`: Now requires `HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m`
5856 * `fill`: Now requires `HasImageWriter` and `HasDisplayRegion`
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`
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`
6260 * `richText`: Now requires `HasImageWriter`, and `HasDisplayRegion`
63− * `scrollableText`: Now requires `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
61+ * `scrollableText`: Now requires `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
6462 * `blank`: Now requires `Monad`
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`
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`
6967 * `checkbox`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
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`
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`
7771* _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
8172 * Add default instances for `HasInput`, `HasFocus`, and `HasImageWriter`
8273 * Export `withinImage` and add `imagesInRegion` to crop images to a region
8374 * Add `anyChildFocused`, which provides information about whether subwidgets are focused
@@ -86,12 +77,6 @@
8677 * Add various `MFunctor` instances
8778 * Add a CPU usage indicator to the example executable
8879
89−## 0.1.4.2
90−
91−* Wider bounds for GHC 8.10 support
92−
93−## 0.1.4.1
94−
9580## 0.1.4.1
9681* Migrate to new dependent-sum / dependent-map (after the "some" package split)
9782
reflex-vty.cabal+7−25
@@ -12,17 +12,12 @@ 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:
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
20+tested-with: GHC ==8.8.4 || ==8.6.5 || ==8.4.4
2621
2722library
2823 exposed-modules: Reflex.Vty
@@ -39,8 +34,8 @@ library
3934 , Reflex.Spider.Orphans
4035 , Control.Monad.NodeId
4136 build-depends:
42− base >= 4.10.0 && < 4.15,
43− bimap >= 0.3.3 && < 0.5,
37+ base >= 4.10.0 && < 4.14,
38+ bimap >= 0.3.3 && < 0.4,
4439 containers >= 0.5.0 && < 0.7,
4540 mtl >= 2.2.2 && < 2.3,
4641 transformers >= 0.5.5 && < 0.6,
@@ -136,19 +131,6 @@ executable example
136131 TypeFamilies
137132
138133
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
134+source-repository head
135+ type: git
136+ location: https://github.com/reflex-frp/reflex-vty
src-bin/Example/CPU.hs+4−2
@@ -99,8 +99,9 @@ cpuStats
9999 , HasImageWriter t m
100100 , HasLayout t m
101101 , HasFocus t m
102+ , MonadNodeId m
102103 , HasInput t m
103− , HasFocusReader t m, HasTheme t m
104+ , HasFocusReader t m
104105 )
105106 => m ()
106107cpuStats = do
@@ -119,10 +120,11 @@ chart
119120 :: ( MonadFix m
120121 , HasFocus t m
121122 , HasLayout t m
123+ , MonadNodeId m
122124 , HasImageWriter t m
123125 , HasInput t m
124126 , HasDisplayRegion t m
125− , HasFocusReader t m, HasTheme t m
127+ , HasFocusReader t m
126128 )
127129 => Dynamic t (Ratio Word64) -> m ()
128130chart pct = do
src-bin/example.hs+6−12
@@ -1,6 +1,7 @@
11import Control.Applicative
22import Control.Monad
33import Control.Monad.Fix
4+import Control.Monad.NodeId
45import Data.Functor
56import Data.Functor.Misc
67import Data.Map (Map)
@@ -21,9 +22,10 @@ type VtyExample t m =
2122 , Reflex t
2223 , HasInput t m
2324 , HasImageWriter t m
25+ , MonadNodeId m
2426 , HasDisplayRegion t m
2527 , HasFocus t m
26− , HasFocusReader t m, HasTheme t m
28+ , HasFocusReader t m
2729 )
2830
2931type Manager t m =
@@ -46,20 +48,12 @@ withCtrlC f = do
4648 V.EvKey (V.KChar 'c') [V.MCtrl] -> Just ()
4749 _ -> Nothing
4850
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−
5751main :: IO ()
5852main = mainWidget $ withCtrlC $ do
5953 initManager_ $ do
6054 tabNavigation
6155 let gf = grout . fixed
62− t = tile flex
56+ t = tile flex
6357 buttons = col $ do
6458 gf 3 $ col $ do
6559 gf 1 $ text "Select an example."
@@ -85,7 +79,7 @@ main = mainWidget $ withCtrlC $ do
8579 _ -> Nothing
8680 rec out <- networkHold buttons $ ffor (switch (current out)) $ \case
8781 Left Example_Todo -> escapable taskList
88− Left Example_TextEditor -> escapable $ localTheme (const (constant darkTheme)) testBoxes
82+ Left Example_TextEditor -> escapable testBoxes
8983 Left Example_ScrollableTextDisplay -> escapable scrolling
9084 Left Example_ClickButtonsGetEmojis -> escapable easyExample
9185 Left Example_CPUStat -> escapable cpuStats
@@ -241,7 +235,7 @@ scrolling = col $ do
241235-- * Text editor example with resizable boxes
242236
243237testBoxes
244− :: (MonadHold t m, VtyExample t m)
238+ :: (MonadHold t m, MonadNodeId m, VtyExample t m)
245239 => m ()
246240testBoxes = do
247241 dw <- displayWidth
src/Data/Text/Zipper.hs+157−280
@@ -9,26 +9,22 @@ 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)
1612import Data.Char (isSpace)
1713import Data.Map (Map)
14+import qualified Data.Map as Map
1815import Data.Maybe (fromMaybe)
1916import Data.String
17+import Control.Monad.State (evalState, forM, get, put)
18+
2019import 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
2825
2926import Graphics.Text.Width (wcwidth)
3027
31−
3228-- | A zipper of the logical text input contents (the "document"). The lines
3329-- before the line containing the cursor are stored in reverse order.
3430-- The cursor is logically between the "before" and "after" text.
@@ -170,7 +166,7 @@ deleteLeftWord (TextZipper lb b a la) =
170166-- | Insert up to n spaces to get to the next logical column that is a multiple of n
171167tab :: Int -> TextZipper -> TextZipper
172168tab n z@(TextZipper _ b _ _) =
173− insert (T.replicate (fromEnum $ n - T.length b `mod` max 1 n) " ") z
169+ insert (T.replicate (fromEnum $ n - (T.length b `mod` max 1 n)) " ") z
174170
175171-- | The plain text contents of the zipper
176172value :: TextZipper -> Text
@@ -193,35 +189,105 @@ fromText = flip insert empty
193189data Span tag = Span tag Text
194190 deriving (Show)
195191
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−
217192-- | Information about the document as it is displayed (i.e., post-wrapping)
218193data DisplayLines tag = DisplayLines
219194 { _displayLines_spans :: [[Span tag]]
220− , _displayLines_offsetMap :: OffsetMapWithAlignment
221− , _displayLines_cursorPos :: (Int, Int) -- cursor position relative to upper left hand corner
195+ , _displayLines_offsetMap :: Map Int Int
196+ , _displayLines_cursorY :: Int
222197 }
223198 deriving (Show)
224199
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+
225291-- | Split a 'Text' at the given column index. For example
226292--
227293-- > splitAtWidth 3 "ᄀabc" == ("ᄀa", "bc")
@@ -231,16 +297,16 @@ splitAtWidth :: Int -> Text -> (Text, Text)
231297splitAtWidth n t@(Text arr off len)
232298 | n <= 0 = (T.empty, t)
233299 | n >= textWidth t = (t, T.empty)
234− | otherwise = let k = toLogicalIndex n t
300+ | otherwise = let k = iterNWidth n t
235301 in (text arr off k, text arr (off+k) (len-k))
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
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
244310
245311-- | Takes the given number of columns of characters. For example
246312--
@@ -269,6 +335,56 @@ dropWidth n = snd . splitAtWidth n
269335charWidth :: Char -> Int
270336charWidth = wcwidth
271337
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+
272388-- | Get the width of the text in a set of 'Span's, taking into account unicode character widths
273389spansWidth :: [Span tag] -> Int
274390spansWidth = sum . map (\(Span _ t) -> textWidth t)
@@ -304,242 +420,3 @@ charIndexAt pos (Stream next s0 _len) = loop_length 0 0 s0
304420 Yield c s' -> if w > pos then i else loop_length (i+1) w s' where
305421 w = z + charWidth c
306422{-# 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/Vty/Widget.hs+11−88
@@ -35,7 +35,6 @@ mainWidgetWithHandle
3535 , HasDisplayRegion t m
3636 , HasFocusReader t m
3737 , HasInput t m
38− , HasTheme t m
3938 ) => m (Event t ()))
4039 -> IO ()
4140mainWidgetWithHandle vty child =
@@ -46,14 +45,12 @@ mainWidgetWithHandle vty child =
4645 let inp' = fforMaybe inp $ \case
4746 V.EvResize {} -> Nothing
4847 x -> Just x
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
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
5754 return $ VtyResult
5855 { _vtyResult_picture = fmap (V.picForLayers . reverse) images
5956 , _vtyResult_shutdown = shutdown
@@ -72,7 +69,6 @@ mainWidget
7269 , MonadNodeId m
7370 , HasDisplayRegion t m
7471 , HasFocusReader t m
75− , HasTheme t m
7672 , HasInput t m
7773 ) => m (Event t ()))
7874 -> IO ()
@@ -228,18 +224,14 @@ data Region = Region
228224 }
229225 deriving (Show, Read, Eq, Ord)
230226
231−-- | A region that occupies no space.
232−nilRegion :: Region
233−nilRegion = Region 0 0 0 0
234−
235227-- | The width and height of a 'Region'
236228regionSize :: Region -> (Int, Int)
237229regionSize (Region _ _ w h) = (w, h)
238230
239231-- | Produces an 'Image' that fills a region with space characters
240−regionBlankImage :: V.Attr -> Region -> Image
241−regionBlankImage attr r@(Region _ _ width height) =
242− withinImage r $ V.charFill attr ' ' width height
232+regionBlankImage :: Region -> Image
233+regionBlankImage r@(Region _ _ width height) =
234+ withinImage r $ V.charFill V.defAttr ' ' width height
243235
244236-- | A class for things that know their own display size dimensions
245237class (Reflex t, Monad m) => HasDisplayRegion t m | m -> t where
@@ -407,7 +399,7 @@ newtype ImageWriter t m a = ImageWriter
407399 , NotReady t
408400 , PerformEvent t
409401 , PostBuild t
410− , TriggerEvent t
402+ , TriggerEvent t
411403 )
412404
413405instance MonadTrans (ImageWriter t) where
@@ -445,75 +437,6 @@ runImageWriter
445437 -> m (a, Behavior t [Image])
446438runImageWriter = runBehaviorWriterT . unImageWriter
447439
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−
517440-- ** Manipulating images
518441
519442-- | Translates and crops an 'Image' so that it is contained by
@@ -544,7 +467,7 @@ imagesInRegion reg = liftA2 (\r is -> map (withinImage r) is) reg
544467-- * mouse inputs inside the region have their coordinates translated such
545468-- that (0,0) is the top-left corner of the region
546469pane
547− :: (Reflex t, Monad m, HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
470+ :: (Reflex t, Monad m, MonadNodeId m, HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
548471 => Dynamic t Region
549472 -> Dynamic t Bool -- ^ Whether the widget should be focused when the parent is.
550473 -> m a
src/Reflex/Vty/Widget/Box.hs+17−19
@@ -3,6 +3,7 @@
33-}
44module Reflex.Vty.Widget.Box where
55
6+import Control.Monad.NodeId
67import Data.Default
78import Data.Text (Text)
89import qualified Data.Text as T
@@ -13,7 +14,7 @@ import Reflex.Vty.Widget
1314import Reflex.Vty.Widget.Text
1415
1516-- | Fill the background with the bottom box style
16−hRule :: (HasDisplayRegion t m, HasImageWriter t m, HasTheme t m) => BoxStyle -> m ()
17+hRule :: (HasDisplayRegion t m, HasImageWriter t m) => BoxStyle -> m ()
1718hRule boxStyle = fill $ pure (_boxStyle_s boxStyle)
1819
1920-- | Defines a set of symbols to use to draw the outlines of boxes
@@ -54,7 +55,7 @@ roundedBoxStyle :: BoxStyle
5455roundedBoxStyle = BoxStyle '╭' '─' '╮' '│' '╯' '─' '╰' '│'
5556
5657-- | Draws a titled box in the provided style and a child widget inside of that box
57−boxTitle :: (Monad m, Reflex t ,HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m, HasTheme t m)
58+boxTitle :: (Monad m, Reflex t, MonadNodeId m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m)
5859 => Behavior t BoxStyle
5960 -> Behavior t Text
6061 -> m a
@@ -62,39 +63,36 @@ boxTitle :: (Monad m, Reflex t ,HasDisplayRegion t m, HasImageWriter t m, HasInp
6263boxTitle boxStyle title child = do
6364 dh <- displayHeight
6465 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−
69− tellImages (boxImages <$> bt <*> title <*> boxStyle <*> current boxReg)
70− tellImages (ffor2 (current innerReg) bt (\r attr -> [regionBlankImage attr r]))
71−
68+ tellImages (boxImages <$> title <*> boxStyle <*> current boxReg)
69+ tellImages (fmap (\r -> [regionBlankImage r]) (current innerReg))
7270 pane innerReg (pure True) child
7371 where
74− boxImages :: V.Attr -> Text -> BoxStyle -> Region -> [Image]
75− boxImages attr title' style (Region left top width height) =
72+ boxImages :: Text -> BoxStyle -> Region -> [Image]
73+ boxImages title' style (Region left top width height) =
7674 let right = left + width - 1
7775 bottom = top + height - 1
7876 sides =
7977 [ withinImage (Region (left + 1) top (width - 2) 1) $
80− V.text' attr $
78+ V.text' V.defAttr $
8179 hPadText title' (_boxStyle_n style) (width - 2)
8280 , withinImage (Region right (top + 1) 1 (height - 2)) $
83− V.charFill attr (_boxStyle_e style) 1 (height - 2)
81+ V.charFill V.defAttr (_boxStyle_e style) 1 (height - 2)
8482 , withinImage (Region (left + 1) bottom (width - 2) 1) $
85− V.charFill attr (_boxStyle_s style) (width - 2) 1
83+ V.charFill V.defAttr (_boxStyle_s style) (width - 2) 1
8684 , withinImage (Region left (top + 1) 1 (height - 2)) $
87− V.charFill attr (_boxStyle_w style) 1 (height - 2)
85+ V.charFill V.defAttr (_boxStyle_w style) 1 (height - 2)
8886 ]
8987 corners =
9088 [ withinImage (Region left top 1 1) $
91− V.char attr (_boxStyle_nw style)
89+ V.char V.defAttr (_boxStyle_nw style)
9290 , withinImage (Region right top 1 1) $
93− V.char attr (_boxStyle_ne style)
91+ V.char V.defAttr (_boxStyle_ne style)
9492 , withinImage (Region right bottom 1 1) $
95− V.char attr (_boxStyle_se style)
93+ V.char V.defAttr (_boxStyle_se style)
9694 , withinImage (Region left bottom 1 1) $
97− V.char attr (_boxStyle_sw style)
95+ V.char V.defAttr (_boxStyle_sw style)
9896 ]
9997 in sides ++ if width > 1 && height > 1 then corners else []
10098 hPadText :: T.Text -> Char -> Int -> T.Text
@@ -109,7 +107,7 @@ boxTitle boxStyle title child = do
109107 right = mkHalf delta
110108
111109-- | A box without a title
112−box :: (Monad m, Reflex t, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m, HasTheme t m)
110+box :: (Monad m, Reflex t, MonadNodeId m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m)
113111 => Behavior t BoxStyle
114112 -> m a
115113 -> m a
@@ -117,7 +115,7 @@ box boxStyle = boxTitle boxStyle mempty
117115
118116-- | A box whose style is static
119117boxStatic
120− :: (Monad m, Reflex t, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m, HasTheme t m)
118+ :: (Monad m, Reflex t, MonadNodeId m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m)
121119 => BoxStyle
122120 -> m a
123121 -> m a
src/Reflex/Vty/Widget/Input.hs+8−9
@@ -13,6 +13,7 @@ import Reflex.Vty.Widget.Input.Text as Export
1313
1414import Control.Monad (join)
1515import Control.Monad.Fix (MonadFix)
16+import Control.Monad.NodeId (MonadNodeId)
1617import Data.Default (Default(..))
1718import Data.Text (Text)
1819import qualified Graphics.Vty as V
@@ -34,7 +35,7 @@ instance Reflex t => Default (ButtonConfig t) where
3435
3536-- | A button widget that contains a sub-widget
3637button
37− :: (Reflex t, Monad m, HasFocusReader t m, HasTheme t m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m)
38+ :: (Reflex t, Monad m, MonadNodeId m, HasFocusReader t m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m)
3839 => ButtonConfig t
3940 -> m ()
4041 -> m (Event t ())
@@ -52,7 +53,7 @@ button cfg child = do
5253
5354-- | A button widget that displays text that can change
5455textButton
55− :: (Reflex t, Monad m, HasDisplayRegion t m, HasFocusReader t m, HasTheme t m, HasImageWriter t m, HasInput t m)
56+ :: (Reflex t, Monad m, MonadNodeId m, HasDisplayRegion t m, HasFocusReader t m, HasImageWriter t m, HasInput t m)
5657 => ButtonConfig t
5758 -> Behavior t Text
5859 -> m (Event t ())
@@ -60,7 +61,7 @@ textButton cfg = button cfg . text -- TODO Centering etc.
6061
6162-- | A button widget that displays a static bit of text
6263textButtonStatic
63− :: (Reflex t, Monad m, HasDisplayRegion t m, HasFocusReader t m, HasTheme t m, HasImageWriter t m, HasInput t m)
64+ :: (Reflex t, Monad m, MonadNodeId m, HasDisplayRegion t m, HasFocusReader t m, HasImageWriter t m, HasInput t m)
6465 => ButtonConfig t
6566 -> Text
6667 -> m (Event t ())
@@ -70,20 +71,19 @@ textButtonStatic cfg = textButton cfg . pure
7071
7172-- | A clickable link widget
7273link
73− :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasTheme t m)
74+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m)
7475 => Behavior t Text
7576 -> m (Event t MouseUp)
7677link t = do
77− bt <- theme
7878 let cfg = RichTextConfig
79− { _richTextConfig_attributes = fmap (\attr -> V.withStyle attr V.underline) bt
79+ { _richTextConfig_attributes = pure $ V.withStyle V.defAttr V.underline
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, HasTheme t m)
86+ :: (Reflex t, Monad m, HasImageWriter t m, HasDisplayRegion t m, HasInput t m)
8787 => Text
8888 -> m (Event t MouseUp)
8989linkStatic = link . pure
@@ -116,7 +116,6 @@ 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
120119 , _checkboxConfig_attributes :: Behavior t V.Attr
121120 , _checkboxConfig_setValue :: Event t Bool
122121 }
@@ -130,7 +129,7 @@ instance (Reflex t) => Default (CheckboxConfig t) where
130129
131130-- | A checkbox widget
132131checkbox
133− :: (MonadHold t m, MonadFix m, Reflex t, HasInput t m, HasDisplayRegion t m, HasImageWriter t m, HasFocusReader t m, HasTheme t m)
132+ :: (MonadHold t m, MonadFix m, Reflex t, HasInput t m, HasDisplayRegion t m, HasImageWriter t m, HasFocusReader t m)
134133 => CheckboxConfig t
135134 -> Bool
136135 -> m (Dynamic t Bool)
src/Reflex/Vty/Widget/Input/Text.hs+12−16
@@ -9,6 +9,7 @@ module Reflex.Vty.Widget.Input.Text
99
1010import Control.Monad (join)
1111import Control.Monad.Fix (MonadFix)
12+import Control.Monad.NodeId (MonadNodeId)
1213import Data.Default (Default(..))
1314import Data.Text (Text)
1415import Data.Text.Zipper
@@ -43,7 +44,7 @@ data TextInput t = TextInput
4344
4445-- | A widget that allows text input
4546textInput
46− :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m, HasFocusReader t m, HasTheme t m, HasDisplayRegion t m, HasImageWriter t m, HasDisplayRegion t m)
47+ :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m, HasFocusReader t m, HasDisplayRegion t m, HasImageWriter t m, HasDisplayRegion t m)
4748 => TextInputConfig t
4849 -> m (TextInput t)
4950textInput cfg = do
@@ -51,8 +52,6 @@ textInput cfg = do
5152 f <- focus
5253 dh <- displayHeight
5354 dw <- displayWidth
54− bt <- theme
55− attr0 <- sample bt
5655 rec v <- foldDyn ($) (_textInputConfig_initialValue cfg) $ mergeWith (.)
5756 [ uncurry (updateTextZipper (_textInputConfig_tabWidth cfg)) <$> attach (current dh) i
5857 , _textInputConfig_modify cfg
@@ -61,20 +60,13 @@ textInput cfg = do
6160 goToDisplayLinePosition mx (st + my) dl
6261 ]
6362 click <- mouseDown V.BLeft
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 = (,,)
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)
6865 <$> dw
6966 <*> (mapZipper <$> _textInputConfig_display cfg <*> v)
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
67+ <*> cursorAttrs
7668 img = images . _displayLines_spans <$> rows
77− y <- holdUniqDyn $ fmap snd _displayLines_cursorPos <$> rows
69+ y <- holdUniqDyn $ _displayLines_cursorY <$> rows
7870 let newScrollTop :: Int -> (Int, Int) -> Int
7971 newScrollTop st (h, cursorY)
8072 | cursorY < st = cursorY
@@ -90,7 +82,7 @@ textInput cfg = do
9082
9183-- | A widget that allows multiline text input
9284multilineTextInput
93− :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m, HasFocusReader t m, HasTheme 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)
9486 => TextInputConfig t
9587 -> m (TextInput t)
9688multilineTextInput cfg = do
@@ -108,7 +100,7 @@ multilineTextInput cfg = do
108100-- the computed line count to greedily size the tile when vertically
109101-- oriented, and uses the fallback width when horizontally oriented.
110102textInputTile
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)
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)
112104 => m (TextInput t)
113105 -> Dynamic t Int
114106 -> m (TextInput t)
@@ -120,6 +112,10 @@ textInputTile txt width = do
120112 Orientation_Row -> width
121113 return t
122114
115+-- | Default attributes for the text cursor
116+cursorAttributes :: V.Attr
117+cursorAttributes = V.withStyle V.defAttr V.reverseVideo
118+
123119-- | Turn a set of display line rows into a list of images (one per line)
124120images :: [[Span V.Attr]] -> [V.Image]
125121images = map (V.horizCat . map spanToImage)
src/Reflex/Vty/Widget/Layout.hs+9−14
@@ -133,8 +133,6 @@ 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−
138136instance (Reflex t, MonadFix m, MonadNodeId m) => HasFocus t (Focus t m) where
139137 makeFocus = do
140138 fid <- FocusId <$> lift getNextNodeId
@@ -186,7 +184,7 @@ anyChildFocused f = do
186184
187185-- | Request focus be shifted backward and forward based on tab presses. <Tab>
188186-- shifts focus forward and <Shift+Tab> shifts focus backward.
189−tabNavigation :: (Reflex t, HasInput t m, HasFocus t m) => m ()
187+tabNavigation :: (Reflex t, MonadNodeId m, HasInput t m, HasFocus t m) => m ()
190188tabNavigation = do
191189 fwd <- fmap (const 1) <$> key (V.KChar '\t')
192190 back <- fmap (const (-1)) <$> key V.KBackTab
@@ -244,14 +242,14 @@ data Orientation = Orientation_Column
244242
245243-- | Create a row-oriented 'axis'
246244row
247− :: (Reflex t, MonadFix m, HasLayout t m)
245+ :: (Reflex t, MonadNodeId m, MonadFix m, HasLayout t m)
248246 => m a
249247 -> m a
250248row = axis (pure Orientation_Row) flex
251249
252250-- | Create a column-oriented 'axis'
253251col
254− :: (Reflex t, MonadFix m, HasLayout t m)
252+ :: (Reflex t, MonadNodeId m, MonadFix m, HasLayout t m)
255253 => m a
256254 -> m a
257255col = axis (pure Orientation_Column) flex
@@ -363,7 +361,7 @@ chunk o r (offset, sz) = case o of
363361
364362-- | A class of operations for creating screen layouts.
365363class Monad m => HasLayout t m | m -> t where
366− -- | Starts a parent element in the current layout with the given size
364+ -- | Creates a parent element in the current layout with the given size
367365 -- constraint, which lays out its children according to the provided
368366 -- orientation.
369367 axis :: Dynamic t Orientation -> Dynamic t Constraint -> m a -> m a
@@ -432,20 +430,17 @@ instance (HasDisplayRegion t m, HasImageWriter t m, MonadFix m) => HasImageWrite
432430
433431instance (HasFocusReader t m, Monad m) => HasFocusReader t (Layout t m)
434432
435−instance (HasTheme t m, Monad m) => HasTheme t (Layout t m)
436−
437433instance (Monad m, MonadNodeId m, Reflex t, MonadFix m) => HasLayout t (Layout t m) where
438434 axis o c (Layout x) = Layout $ do
439435 nodeId <- getNextNodeId
440− let dummyParentLayout = LayoutTree (nilRegion, Orientation_Column) mempty
441− (result, forest) <- lift $ local (\t -> fromMaybe dummyParentLayout . 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
442437 tellDyn $ singletonLF nodeId <$> (LayoutTree <$> ((,) <$> c <*> o) <*> forest)
443438 pure result
444439 region c = do
445440 nodeId <- lift getNextNodeId
446441 Layout $ tellDyn $ ffor c $ \c' -> singletonLF nodeId $ LayoutTree (c', Orientation_Row) mempty
447442 solutions <- Layout ask
448− pure $ maybe nilRegion (fst . rootLT) . lookupLF nodeId . childrenLT <$> solutions
443+ pure $ maybe (Region 0 0 0 0) (fst . rootLT) . lookupLF nodeId . childrenLT <$> solutions
449444 askOrientation = Layout $ asks $ fmap (snd . rootLT)
450445
451446instance (MonadFix m, HasFocus t m) => HasFocus t (Layout t m) where
@@ -510,7 +505,7 @@ initManager_ = fmap fst . initManager
510505-- provided constraint. Returns the 'FocusId' allowing for manual focus
511506-- management.
512507tile'
513− :: (MonadFix m, Reflex t, HasInput t m, HasFocus t m, HasLayout t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader 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)
514509 => Dynamic t Constraint
515510 -> m a
516511 -> m (FocusId, a)
@@ -529,7 +524,7 @@ tile' c w = do
529524-- | A widget that is focusable and occupies a layout region based on the
530525-- provided constraint.
531526tile
532− :: (MonadFix m, Reflex t, HasInput t m, HasFocus t m, HasLayout t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader 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)
533528 => Dynamic t Constraint
534529 -> m a
535530 -> m a
@@ -540,7 +535,7 @@ tile c = fmap snd . tile' c
540535-- | A widget that is not focusable and occupies a layout region based on the
541536-- provided constraint.
542537grout
543− :: (Reflex t, HasLayout t m, HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
538+ :: (Reflex t, MonadNodeId m, HasLayout t m, HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
544539 => Dynamic t Constraint
545540 -> m a
546541 -> m a
src/Reflex/Vty/Widget/Split.hs+4−3
@@ -5,6 +5,7 @@ module Reflex.Vty.Widget.Split where
55
66import Control.Applicative
77import Control.Monad.Fix
8+import Control.Monad.NodeId
89import Graphics.Vty as V
910import Reflex
1011import Reflex.Vty.Widget
@@ -13,7 +14,7 @@ import Reflex.Vty.Widget.Input.Mouse
1314-- | A split of the available space into two parts with a draggable separator.
1415-- Starts with half the space allocated to each, and the first pane has focus.
1516-- Clicking in a pane switches focus.
16−splitVDrag :: (Reflex t, MonadFix m, MonadHold t m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m)
17+splitVDrag :: (Reflex t, MonadFix m, MonadHold t m, MonadNodeId m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m)
1718 => m ()
1819 -> m a
1920 -> m b
@@ -52,7 +53,7 @@ splitVDrag wS wA wB = do
5253
5354-- | A plain split of the available space into vertically stacked panes.
5455-- No visual separator is built in here.
55−splitV :: (Reflex t, Monad m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m)
56+splitV :: (Reflex t, Monad m, MonadNodeId m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m)
5657 => Dynamic t (Int -> Int)
5758 -- ^ Function used to determine size of first pane based on available size
5859 -> Dynamic t (Bool, Bool)
@@ -73,7 +74,7 @@ splitV sizeFunD focD wA wB = do
7374
7475-- | A plain split of the available space into horizontally stacked panes.
7576-- No visual separator is built in here.
76−splitH :: (Reflex t, Monad m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m)
77+splitH :: (Reflex t, Monad m, MonadNodeId m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m)
7778 => Dynamic t (Int -> Int)
7879 -- ^ Function used to determine size of first pane based on available size
7980 -> Dynamic t (Bool, Bool)
src/Reflex/Vty/Widget/Text.hs+10−17
@@ -14,15 +14,13 @@ 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, HasTheme t m) => Behavior t Char -> m ()
17+fill :: (HasDisplayRegion t m, HasImageWriter t m) => Behavior t Char -> m ()
1818fill bc = do
1919 dw <- displayWidth
2020 dh <- displayHeight
21− bt <- theme
2221 let fillImg =
23− (\attr w h c -> [V.charFill attr c w h])
24− <$> bt
25− <*> current dw
22+ (\w h c -> [V.charFill V.defAttr c w h])
23+ <$> current dw
2624 <*> current dh
2725 <*> bc
2826 tellImages fillImg
@@ -35,11 +33,9 @@ data RichTextConfig t = RichTextConfig
3533instance Reflex t => Default (RichTextConfig t) where
3634 def = RichTextConfig $ pure V.defAttr
3735
38−
39−-- TODO delete this and use new local theming
4036-- | A widget that displays text with custom time-varying attributes
4137richText
42− :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m, HasTheme t m)
38+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m)
4339 => RichTextConfig t
4440 -> Behavior t Text
4541 -> m ()
@@ -57,17 +53,15 @@ richText cfg t = do
5753
5854-- | Renders text, wrapped to the container width
5955text
60− :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m, HasTheme t m)
56+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m)
6157 => Behavior t Text
6258 -> m ()
63−text t = do
64− bt <- theme
65− richText (RichTextConfig bt) t
59+text = richText def
6660
6761-- | Scrollable text widget. The output pair exposes the current scroll position and total number of lines (including those
6862-- that are hidden)
6963scrollableText
70− :: forall t m. (Reflex t, MonadHold t m, MonadFix m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasTheme t m)
64+ :: forall t m. (Reflex t, MonadHold t m, MonadFix m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m)
7165 => Event t Int
7266 -- ^ Number of lines to scroll by
7367 -> Behavior t Text
@@ -75,8 +69,7 @@ scrollableText
7569 -- ^ (Current scroll position, total number of lines)
7670scrollableText scrollBy t = do
7771 dw <- displayWidth
78− bt <- theme
79− let imgs = wrap <$> bt <*> current dw <*> t
72+ let imgs = wrap <$> current dw <*> t
8073 kup <- key V.KUp
8174 kdown <- key V.KDown
8275 m <- mouseScroll
@@ -95,12 +88,12 @@ scrollableText scrollBy t = do
9588 tellImages $ fmap ((:[]) . V.vertCat) $ drop <$> current lineIndex <*> imgs
9689 return $ (,) <$> ((+) <$> current lineIndex <*> pure 1) <*> (length <$> imgs)
9790 where
98− wrap attr maxWidth = concatMap (fmap (V.string attr . T.unpack) . TZ.wrapWithOffset maxWidth 0) . T.split (=='\n')
91+ wrap maxWidth = concatMap (fmap (V.string V.defAttr . T.unpack) . TZ.wrapWithOffset maxWidth 0) . T.split (=='\n')
9992
10093-- | Renders any behavior whose value can be converted to
10194-- 'String' as text
10295display
103− :: (Reflex t, Monad m, Show a, HasDisplayRegion t m, HasImageWriter t m, HasTheme t m)
96+ :: (Reflex t, Monad m, Show a, HasDisplayRegion t m, HasImageWriter t m)
10497 => Behavior t a
10598 -> m ()
10699display a = text $ T.pack . show <$> a
test/Data/Text/ZipperSpec.hsdeleted−104
@@ -1,104 +0,0 @@
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.hsdeleted−15
@@ -1,15 +0,0 @@
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