on 533265e help sign in
.github/workflows/haskell.yml+10−12
since splitsnapshots
unifiedsplit
@@ -1,33 +1,33 @@
1−name: github-action
1+name: Haskell CI
2
2
3on: [push, pull_request]
3on: [push, pull_request]
4
4
5jobs:
5jobs:
6 build:
6 build:
7+
8+ runs-on: ${{ matrix.os }}
7 strategy:
9 strategy:
8 matrix:
10 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']
10 os: ['ubuntu-latest', 'macos-latest']
12 os: ['ubuntu-latest', 'macos-latest']
11− runs-on: ${{ matrix.os }}
12−
13− name: GHC ${{ matrix.ghc }} on ${{ matrix.os }}
14 steps:
13 steps:
15 - uses: actions/checkout@v2
14 - uses: actions/checkout@v2
16 - uses: actions/setup-haskell@v1
15 - uses: actions/setup-haskell@v1
17 with:
16 with:
18 ghc-version: ${{ matrix.ghc }}
17 ghc-version: ${{ matrix.ghc }}
18+ cabal-version: '3.0'
19+
19 - name: Cache
20 - name: Cache
20 uses: actions/cache@v1
21 uses: actions/cache@v1
21 env:
22 env:
22 cache-name: cache-cabal
23 cache-name: cache-cabal
23 with:
24 with:
24 path: ~/.cabal
25 path: ~/.cabal
25− key: ${{ runner.os }}-${{ matrix.ghc }}-build-${{ env.cache-name }}-${{ hashFiles('**/*.cabal') }}-${{ hashFiles('**/cabal.project') }}
26+ key: ${{ runner.os }}-1-build-${{ env.cache-name }}-${{ hashFiles('**/*.cabal') }}-${{ hashFiles('**/cabal.project') }}
26 restore-keys: |
27 restore-keys: |
27− ${{ runner.os }}-${{ matrix.ghc }}-build-${{ env.cache-name }}-
28+ ${{ runner.os }}-1-build-${{ env.cache-name }}-
28− ${{ runner.os }}-${{ matrix.ghc }}-build-
29+ ${{ runner.os }}-1-build-
29− ${{ runner.os }}-${{ matrix.ghc }}-
30+ ${{ runner.os }}-1-
30− ${{ runner.os }}
31
31
32 - name: Install dependencies
32 - name: Install dependencies
33 run: |
33 run: |
@@ -37,5 +37,3 @@ jobs:
37 run: cabal build --enable-tests --enable-benchmarks all
37 run: cabal build --enable-tests --enable-benchmarks all
38 - name: Run tests
38 - name: Run tests
39 run: cabal test all
39 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−23
@@ -33,8 +33,6 @@
33 * Introduce a `FocusReader` monad transformer
33 * Introduce a `FocusReader` monad transformer
34 * Replace `HasVtyInput` with `HasInput`
34 * Replace `HasVtyInput` with `HasInput`
35 * Introduce an `Input` monad transformer
35 * 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
38 * Remove `DynRegion` and `currentRegion`. Use `Dynamic t Region` and `current` instead. This also changes the type of `pane`'s argument.
36 * Remove `DynRegion` and `currentRegion`. Use `Dynamic t Region` and `current` instead. This also changes the type of `pane`'s argument.
39 * `CheckboxConfig` now has a field taking an `Event` to set the value of the checkbox.
37 * `CheckboxConfig` now has a field taking an `Event` to set the value of the checkbox.
40 * `checkbox` now accepts keyboard input (spacebar to check and uncheck) and is displayed in bold when focused.
38 * `checkbox` now accepts keyboard input (spacebar to check and uncheck) and is displayed in bold when focused.
@@ -56,24 +54,20 @@
56 * `splitH`: Now requires `HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m`
54 * `splitH`: Now requires `HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m`
57 * `splitVDrag`: Now requires `HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m`
55 * `splitVDrag`: Now requires `HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m`
58 * `fill`: Now requires `HasImageWriter` and `HasDisplayRegion`
56 * `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`
57+ * `boxTitle`: Now requires `HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m`
60− * `box`: Now requires `HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m, HasTheme t m`
58+ * `box`: Now requires `HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m`
61− * `boxStatic`: Now requires `HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m, HasTheme t m`
59+ * `boxStatic`: Now requires `HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m`
62 * `richText`: Now requires `HasImageWriter`, and `HasDisplayRegion`
60 * `richText`: Now requires `HasImageWriter`, and `HasDisplayRegion`
63− * `scrollableText`: Now requires `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
61+ * `scrollableText`: Now requires `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
64 * `blank`: Now requires `Monad`
62 * `blank`: Now requires `Monad`
65− * `button`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
63+ * `button`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
66− * `textButton`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
64+ * `textButton`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
67− * `textButtonStatic`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
65+ * `textButtonStatic`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
68− * `link`: Now requires `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
66+ * `link`: Now requires `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
69 * `checkbox`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
67 * `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`
68+ * `textInput`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
75− * `multilineTextInput`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
69+ * `multilineTextInput`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
76− * `textInputTile`: Now requires `HasFocusReader`, `HasInput`, `HasLayout`, `HasTheme`, and `HasFocus`
70+ * `textInputTile`: Now requires `HasFocusReader`, `HasInput`, `HasLayout`, and `HasFocus`
77* _Misc_:
71* _Misc_:
78 * (#40 Add alignment support to TextZipper)[https://github.com/reflex-frp/reflex-vty/pull/40]
72 * (#40 Add alignment support to TextZipper)[https://github.com/reflex-frp/reflex-vty/pull/40]
79 * Add alignment (left/center/right) support to TextZipper
73 * Add alignment (left/center/right) support to TextZipper
@@ -86,12 +80,6 @@
86 * Add various `MFunctor` instances
80 * Add various `MFunctor` instances
87 * Add a CPU usage indicator to the example executable
81 * Add a CPU usage indicator to the example executable
88
82
89−## 0.1.4.2
90−
91−* Wider bounds for GHC 8.10 support
92−
93−## 0.1.4.1
94−
95## 0.1.4.1
83## 0.1.4.1
96* Migrate to new dependent-sum / dependent-map (after the "some" package split)
84* Migrate to new dependent-sum / dependent-map (after the "some" package split)
97
85
reflex-vty.cabal+8−9
@@ -12,17 +12,12 @@ maintainer: maintainer@obsidian.systems
12copyright: 2020 Obsidian Systems LLC
12copyright: 2020 Obsidian Systems LLC
13category: FRP
13category: FRP
14build-type: Simple
14build-type: Simple
15−cabal-version: 1.18
15+cabal-version: >=1.18
16extra-source-files:
16extra-source-files:
17 README.md
17 README.md
18 ChangeLog.md
18 ChangeLog.md
19extra-doc-files: doc/tasks.png
19extra-doc-files: doc/tasks.png
20−tested-with:
20+tested-with: GHC ==8.8.4 || ==8.6.5 || ==8.4.4
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
26
21
27library
22library
28 exposed-modules: Reflex.Vty
23 exposed-modules: Reflex.Vty
@@ -39,8 +34,8 @@ library
39 , Reflex.Spider.Orphans
34 , Reflex.Spider.Orphans
40 , Control.Monad.NodeId
35 , Control.Monad.NodeId
41 build-depends:
36 build-depends:
42− base >= 4.10.0 && < 4.15,
37+ base >= 4.10.0 && < 4.14,
43− bimap >= 0.3.3 && < 0.5,
38+ bimap >= 0.3.3 && < 0.4,
44 containers >= 0.5.0 && < 0.7,
39 containers >= 0.5.0 && < 0.7,
45 mtl >= 2.2.2 && < 2.3,
40 mtl >= 2.2.2 && < 2.3,
46 transformers >= 0.5.5 && < 0.6,
41 transformers >= 0.5.5 && < 0.6,
@@ -152,3 +147,7 @@ test-suite reflex-vty-test
152 extra,
147 extra,
153 hspec >= 2.7 && < 2.8
148 hspec >= 2.7 && < 2.8
154 default-language: Haskell2010
149 default-language: Haskell2010
150+
151+source-repository head
152+ type: git
153+ location: https://github.com/reflex-frp/reflex-vty
src-bin/Example/CPU.hs+2−2
@@ -100,7 +100,7 @@ cpuStats
100 , HasLayout t m
100 , HasLayout t m
101 , HasFocus t m
101 , HasFocus t m
102 , HasInput t m
102 , HasInput t m
103− , HasFocusReader t m, HasTheme t m
103+ , HasFocusReader t m
104 )
104 )
105 => m ()
105 => m ()
106cpuStats = do
106cpuStats = do
@@ -122,7 +122,7 @@ chart
122 , HasImageWriter t m
122 , HasImageWriter t m
123 , HasInput t m
123 , HasInput t m
124 , HasDisplayRegion t m
124 , HasDisplayRegion t m
125− , HasFocusReader t m, HasTheme t m
125+ , HasFocusReader t m
126 )
126 )
127 => Dynamic t (Ratio Word64) -> m ()
127 => Dynamic t (Ratio Word64) -> m ()
128chart pct = do
128chart pct = do
src-bin/example.hs+3−11
@@ -23,7 +23,7 @@ type VtyExample t m =
23 , HasImageWriter t m
23 , HasImageWriter t m
24 , HasDisplayRegion t m
24 , HasDisplayRegion t m
25 , HasFocus t m
25 , HasFocus t m
26− , HasFocusReader t m, HasTheme t m
26+ , HasFocusReader t m
27 )
27 )
28
28
29type Manager t m =
29type Manager t m =
@@ -46,20 +46,12 @@ withCtrlC f = do
46 V.EvKey (V.KChar 'c') [V.MCtrl] -> Just ()
46 V.EvKey (V.KChar 'c') [V.MCtrl] -> Just ()
47 _ -> Nothing
47 _ -> Nothing
48
48
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−
57main :: IO ()
49main :: IO ()
58main = mainWidget $ withCtrlC $ do
50main = mainWidget $ withCtrlC $ do
59 initManager_ $ do
51 initManager_ $ do
60 tabNavigation
52 tabNavigation
61 let gf = grout . fixed
53 let gf = grout . fixed
62− t = tile flex
54+ t = tile flex
63 buttons = col $ do
55 buttons = col $ do
64 gf 3 $ col $ do
56 gf 3 $ col $ do
65 gf 1 $ text "Select an example."
57 gf 1 $ text "Select an example."
@@ -85,7 +77,7 @@ main = mainWidget $ withCtrlC $ do
85 _ -> Nothing
77 _ -> Nothing
86 rec out <- networkHold buttons $ ffor (switch (current out)) $ \case
78 rec out <- networkHold buttons $ ffor (switch (current out)) $ \case
87 Left Example_Todo -> escapable taskList
79 Left Example_Todo -> escapable taskList
88− Left Example_TextEditor -> escapable $ localTheme (const (constant darkTheme)) testBoxes
80+ Left Example_TextEditor -> escapable testBoxes
89 Left Example_ScrollableTextDisplay -> escapable scrolling
81 Left Example_ScrollableTextDisplay -> escapable scrolling
90 Left Example_ClickButtonsGetEmojis -> escapable easyExample
82 Left Example_ClickButtonsGetEmojis -> escapable easyExample
91 Left Example_CPUStat -> escapable cpuStats
83 Left Example_CPUStat -> escapable cpuStats
src/Data/Text/Zipper.hs+195−82
@@ -12,7 +12,7 @@ module Data.Text.Zipper where
12import Prelude
12import Prelude
13
13
14import Control.Exception (assert)
14import Control.Exception (assert)
15−import Control.Monad.State (evalState, forM, get, put)
15+import Control.Monad.State (evalState, forM, get, put, join)
16import Data.Char (isSpace)
16import Data.Char (isSpace)
17import Data.Map (Map)
17import Data.Map (Map)
18import Data.Maybe (fromMaybe)
18import Data.Maybe (fromMaybe)
@@ -170,7 +170,7 @@ deleteLeftWord (TextZipper lb b a la) =
170-- | Insert up to n spaces to get to the next logical column that is a multiple of n
170-- | Insert up to n spaces to get to the next logical column that is a multiple of n
171tab :: Int -> TextZipper -> TextZipper
171tab :: Int -> TextZipper -> TextZipper
172tab n z@(TextZipper _ b _ _) =
172tab n z@(TextZipper _ b _ _) =
173− 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
174
174
175-- | The plain text contents of the zipper
175-- | The plain text contents of the zipper
176value :: TextZipper -> Text
176value :: TextZipper -> Text
@@ -193,35 +193,105 @@ fromText = flip insert empty
193data Span tag = Span tag Text
193data Span tag = Span tag Text
194 deriving (Show)
194 deriving (Show)
195
195
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−
217-- | Information about the document as it is displayed (i.e., post-wrapping)
196-- | Information about the document as it is displayed (i.e., post-wrapping)
218data DisplayLines tag = DisplayLines
197data DisplayLines tag = DisplayLines
219 { _displayLines_spans :: [[Span tag]]
198 { _displayLines_spans :: [[Span tag]]
220− , _displayLines_offsetMap :: OffsetMapWithAlignment
199+ , _displayLines_offsetMap :: Map Int Int
221− , _displayLines_cursorPos :: (Int, Int) -- cursor position relative to upper left hand corner
200+ , _displayLines_cursorY :: Int
222 }
201 }
223 deriving (Show)
202 deriving (Show)
224
203
204+-- | Given a width and a 'TextZipper', produce a list of display lines
205+-- (i.e., lines of wrapped text) with special attributes applied to
206+-- certain segments (e.g., the cursor). Additionally, produce the current
207+-- y-coordinate of the cursor and a mapping from display line number to text
208+-- offset
209+displayLines
210+ :: Int -- ^ Width, used for wrapping
211+ -> tag -- ^ Metadata for normal characters
212+ -> tag -- ^ Metadata for the cursor
213+ -> TextZipper -- ^ The text input contents and cursor state
214+ -> DisplayLines tag
215+displayLines width tag cursorTag (TextZipper lb b a la) =
216+ let linesBefore :: [[Text]] -- The wrapped lines before the cursor line
217+ linesBefore = map (wrapWithOffset width 0) $ reverse lb
218+ linesAfter :: [[Text]] -- The wrapped lines after the cursor line
219+ linesAfter = map (wrapWithOffset width 0) la
220+ offsets :: Map Int Int
221+ offsets = offsetMap $ mconcat
222+ [ linesBefore
223+ , [wrapWithOffset width 0 $ b <> a]
224+ , linesAfter
225+ ]
226+ spansBefore = map ((:[]) . Span tag) $ concat linesBefore
227+ spansAfter = map ((:[]) . Span tag) $ concat linesAfter
228+ -- Separate the spans before the cursor into
229+ -- * spans that are on earlier display lines (though on the same logical line), and
230+ -- * spans that are on the same display line
231+ (spansCurrentBefore, spansCurLineBefore) = fromMaybe ([], []) $
232+ initLast $ map ((:[]) . Span tag) (wrapWithOffset width 0 b)
233+ -- Calculate the number of columns on the cursor's display line before the cursor
234+ curLineOffset = spansWidth spansCurLineBefore
235+ -- Check whether the spans on the current display line are long enough that
236+ -- the cursor has to go to the next line
237+ cursorAfterEOL = curLineOffset == width
238+ cursorCharWidth = case T.uncons a of
239+ Nothing -> 1
240+ Just (c, _) -> charWidth c
241+ -- Separate the span after the cursor into
242+ -- * spans that are on the same display line, and
243+ -- * spans that are on later display lines (though on the same logical line)
244+ (spansCurLineAfter, spansCurrentAfter) = fromMaybe ([], []) $
245+ headTail $ case T.uncons a of
246+ Nothing -> [[Span cursorTag " "]]
247+ Just (c, rest) ->
248+ let o = if cursorAfterEOL then cursorCharWidth else curLineOffset + cursorCharWidth
249+ cursor = Span cursorTag (T.singleton c)
250+ in case map ((:[]) . Span tag) (wrapWithOffset width o rest) of
251+ [] -> [[cursor]]
252+ (l:ls) -> (cursor : l) : ls
253+ in DisplayLines
254+ { _displayLines_spans = concat
255+ [ spansBefore
256+ , spansCurrentBefore
257+ , if cursorAfterEOL
258+ then [ spansCurLineBefore, spansCurLineAfter ]
259+ else [ spansCurLineBefore <> spansCurLineAfter ]
260+ , spansCurrentAfter
261+ , spansAfter
262+ ]
263+ , _displayLines_offsetMap = offsets
264+ , _displayLines_cursorY = sum
265+ [ length spansBefore
266+ , length spansCurrentBefore
267+ , if cursorAfterEOL then 1 else 0
268+ ]
269+ }
270+ where
271+ initLast :: [a] -> Maybe ([a], a)
272+ initLast = \case
273+ [] -> Nothing
274+ (x:xs) -> case initLast xs of
275+ Nothing -> Just ([], x)
276+ Just (ys, y) -> Just (x:ys, y)
277+ headTail :: [a] -> Maybe (a, [a])
278+ headTail = \case
279+ [] -> Nothing
280+ x:xs -> Just (x, xs)
281+
282+-- | Wraps a logical line of text to fit within the given width. The first
283+-- wrapped line is offset by the number of columns provided. Subsequent wrapped
284+-- lines are not.
285+wrapWithOffset
286+ :: Int -- ^ Maximum width
287+ -> Int -- ^ Offset for first line
288+ -> Text -- ^ Text to be wrapped
289+ -> [Text]
290+wrapWithOffset maxWidth _ _ | maxWidth <= 0 = []
291+wrapWithOffset maxWidth n xs =
292+ let (firstLine, rest) = splitAtWidth (maxWidth - n) xs
293+ in firstLine : (fmap (takeWidth maxWidth) . takeWhile (not . T.null) . iterate (dropWidth maxWidth) $ rest)
294+
225-- | Split a 'Text' at the given column index. For example
295-- | Split a 'Text' at the given column index. For example
226--
296--
227-- > splitAtWidth 3 "ᄀabc" == ("ᄀa", "bc")
297-- > splitAtWidth 3 "ᄀabc" == ("ᄀa", "bc")
@@ -269,6 +339,56 @@ dropWidth n = snd . splitAtWidth n
269charWidth :: Char -> Int
339charWidth :: Char -> Int
270charWidth = wcwidth
340charWidth = wcwidth
271
341
342+
343+-- | For a given set of wrapped logical lines, computes a map
344+-- from display line index to text offset in the original text.
345+-- This is used to help determine how interactions with the displayed
346+-- text map back to the original text.
347+-- For example, given the document @\"AA\\nBBB\\nCCCCCCCC\\n\"@ wrapped to 5 columns,
348+-- this function will compute the offset in the original document of each character
349+-- in column 1:
350+--
351+-- > AA... (0, 0)
352+-- > BBB.. (1, 3)
353+-- > CCCCC (2, 7) -- (this line wraps to the next row)
354+-- > CCC.. (3, 12)
355+-- > ..... (4, 16)
356+offsetMap
357+ :: [[Text]] -- ^ The outer list represents logical lines, and the
358+ -- inner list represents the display lines into which
359+ -- the logical line has been wrapped
360+ -> Map Int Int -- ^ A map from the index (row) of display line to
361+ -- the text offset from the beginning of the document
362+ -- to the first character of the display line
363+offsetMap ts = evalState (offsetMap' ts) (0, 0)
364+ where
365+ offsetMap' xs = fmap Map.unions $ forM xs $ \x -> do
366+ maps <- forM x $ \line -> do
367+ let l = T.length line
368+ (dl, o) <- get
369+ put (dl + 1, o + l)
370+ return $ Map.singleton dl o
371+ (dl, o) <- get
372+ put (dl, o + 1)
373+ return $ Map.insert dl (o + 1) $ Map.unions maps
374+
375+-- | Move the cursor of the given 'TextZipper' to the logical position indicated
376+-- by the given display line coordinates, using the provided 'DisplayLines'
377+-- information. If the x coordinate is beyond the end of a line, the cursor is
378+-- moved to the end of the line.
379+goToDisplayLinePosition :: Int -> Int -> DisplayLines tag -> TextZipper -> TextZipper
380+goToDisplayLinePosition x y dl tz =
381+ let offset = Map.lookup y $ _displayLines_offsetMap dl
382+ in case offset of
383+ Nothing -> tz
384+ Just o ->
385+ let
386+ moveRight = case drop y $ _displayLines_spans dl of
387+ [] -> x
388+ (s:_) -> charIndexAt x . stream . mconcat . fmap (\(Span _ t) -> t) $ s
389+ in rightN (o + min moveRight x) $ top tz
390+
391+
272-- | Get the width of the text in a set of 'Span's, taking into account unicode character widths
392-- | Get the width of the text in a set of 'Span's, taking into account unicode character widths
273spansWidth :: [Span tag] -> Int
393spansWidth :: [Span tag] -> Int
274spansWidth = sum . map (\(Span _ t) -> textWidth t)
394spansWidth = sum . map (\(Span _ t) -> textWidth t)
@@ -314,7 +434,9 @@ wordsWithWhitespace :: Text -> [Text]
314wordsWithWhitespace t@(Text arr off len) = loop 0 0 False
434wordsWithWhitespace t@(Text arr off len) = loop 0 0 False
315 where
435 where
316 loop !start !n !wasSpace
436 loop !start !n !wasSpace
437+ | n >= len = if start == n
438+ then []
317− | n >= len = [Text arr (start+off) (n-start) | not (start == n)]
439+ else [Text arr (start+off) (n-start)]
318 | isSpace c = loop start (n+d) True
440 | isSpace c = loop start (n+d) True
319 | wasSpace = Text arr (start+off) (n-start) : loop n n False
441 | wasSpace = Text arr (start+off) (n-start) : loop n n False
320 | otherwise = loop start (n+d) False
442 | otherwise = loop start (n+d) False
@@ -352,6 +474,33 @@ splitWordsAtDisplayWidth maxWidth wwws = reverse $ loop wwws 0 [] where
352 else loop (x:xs) 0 [] <> modifyOutForNewLine out
474 else loop (x:xs) 0 [] <> modifyOutForNewLine out
353 else loop xs newWidth $ appendOut out x False
475 else loop xs newWidth $ appendOut out x False
354
476
477+data TextAlignment =
478+ TextAlignment_Left
479+ | TextAlignment_Right
480+ | TextAlignment_Center
481+ deriving (Eq, Show)
482+
483+-- A map from the index (row) of display line to
484+-- fst: leading empty spaces from left (may be negative) to adjust for alignment
485+-- snd: the text offset from the beginning of the document
486+-- to the first character of the display line
487+type OffsetMapWithAlignment = Map Int (Int, Int)
488+
489+-- | Information about the document as it is displayed (i.e., post-wrapping)
490+data DisplayLinesWithAlignment tag = DisplayLinesWithAlignment
491+ { _displayLinesWithAlignment_spans :: [[Span tag]]
492+ , _displayLinesWithAlignment_offsetMap :: OffsetMapWithAlignment
493+ -- cursor position relative to upper left hand corner
494+ , _displayLinesWithAlignment_cursorPos :: (Int, Int) -- cursor position relative to upper left hand corner
495+ }
496+ deriving (Show)
497+
498+data WrappedLine = WrappedLine
499+ { _wrappedLines_text :: Text
500+ , _wrappedLines_hiddenWhitespace :: Bool -- ^ 'True' if this line ends with a deleted whitespace character
501+ , _wrappedLines_offset :: Int -- ^ offset from beginning of line
502+ }
503+ deriving (Eq, Show)
355
504
356
505
357-- | Wraps a logical line of text to fit within the given width. The first
506-- | Wraps a logical line of text to fit within the given width. The first
@@ -365,8 +514,7 @@ wrapWithOffsetAndAlignment
365 -> [WrappedLine] -- (words on that line, hidden space char, offset from beginning of line)
514 -> [WrappedLine] -- (words on that line, hidden space char, offset from beginning of line)
366wrapWithOffsetAndAlignment _ maxWidth _ _ | maxWidth <= 0 = []
515wrapWithOffsetAndAlignment _ maxWidth _ _ | maxWidth <= 0 = []
367wrapWithOffsetAndAlignment alignment maxWidth n txt = assert (n <= maxWidth) r where
516wrapWithOffsetAndAlignment 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)
517+ r' = splitWordsAtDisplayWidth maxWidth $ T.replicate n " " : wordsWithWhitespace txt
370 fmapfn (t,b) = case alignment of
518 fmapfn (t,b) = case alignment of
371 TextAlignment_Left -> WrappedLine t b 0
519 TextAlignment_Left -> WrappedLine t b 0
372 TextAlignment_Right -> WrappedLine t b (maxWidth-l)
520 TextAlignment_Right -> WrappedLine t b (maxWidth-l)
@@ -379,7 +527,7 @@ wrapWithOffsetAndAlignment alignment maxWidth n txt = assert (n <= maxWidth) r w
379
527
380-- converts deleted eol spaces into logical lines
528-- converts deleted eol spaces into logical lines
381eolSpacesToLogicalLines :: [[WrappedLine]] -> [[(Text, Int)]]
529eolSpacesToLogicalLines :: [[WrappedLine]] -> [[(Text, Int)]]
382−eolSpacesToLogicalLines = fmap (fmap (\(WrappedLine a _ c) -> (a,c))) . concatMap (L.groupBy (\(WrappedLine _ b _) _ -> not b))
530+eolSpacesToLogicalLines = fmap (fmap (\(WrappedLine a _ c) -> (a,c))) . join . fmap (L.groupBy (\(WrappedLine _ b _) _ -> not b))
383
531
384offsetMapWithAlignmentInternal :: [[WrappedLine]] -> OffsetMapWithAlignment
532offsetMapWithAlignmentInternal :: [[WrappedLine]] -> OffsetMapWithAlignment
385offsetMapWithAlignmentInternal = offsetMapWithAlignment . eolSpacesToLogicalLines
533offsetMapWithAlignmentInternal = offsetMapWithAlignment . eolSpacesToLogicalLines
@@ -407,24 +555,21 @@ offsetMapWithAlignment ts = evalState (offsetMap' ts) (0, 0)
407-- y-coordinate of the cursor and a mapping from display line number to text
555-- y-coordinate of the cursor and a mapping from display line number to text
408-- offset
556-- offset
409displayLinesWithAlignment
557displayLinesWithAlignment
410− :: TextAlignment
558+ :: (Show tag) => TextAlignment
411 -> Int -- ^ Width, used for wrapping
559 -> Int -- ^ Width, used for wrapping
412 -> tag -- ^ Metadata for normal characters
560 -> tag -- ^ Metadata for normal characters
413 -> tag -- ^ Metadata for the cursor
561 -> tag -- ^ Metadata for the cursor
414 -> TextZipper -- ^ The text input contents and cursor state
562 -> TextZipper -- ^ The text input contents and cursor state
415− -> DisplayLines tag
563+ -> DisplayLinesWithAlignment tag
416displayLinesWithAlignment alignment width tag cursorTag (TextZipper lb b a la) =
564displayLinesWithAlignment alignment width tag cursorTag (TextZipper lb b a la) =
417 let linesBefore :: [[WrappedLine]] -- The wrapped lines before the cursor line
565 let linesBefore :: [[WrappedLine]] -- The wrapped lines before the cursor line
418 linesBefore = map (wrapWithOffsetAndAlignment alignment width 0) $ reverse lb
566 linesBefore = map (wrapWithOffsetAndAlignment alignment width 0) $ reverse lb
419 linesAfter :: [[WrappedLine]] -- The wrapped lines after the cursor line
567 linesAfter :: [[WrappedLine]] -- The wrapped lines after the cursor line
420 linesAfter = map (wrapWithOffsetAndAlignment alignment width 0) la
568 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
569 offsets :: OffsetMapWithAlignment
425 offsets = offsetMapWithAlignmentInternal $ mconcat
570 offsets = offsetMapWithAlignmentInternal $ mconcat
426 [ linesBefore
571 [ linesBefore
427− , [wrapWithOffsetAndAlignment alignment width 0 $ b <> afterWithCursor]
572+ , [wrapWithOffsetAndAlignment alignment width 0 $ b <> a]
428 , linesAfter
573 , linesAfter
429 ]
574 ]
430 flattenLines = concatMap (fmap _wrappedLines_text)
575 flattenLines = concatMap (fmap _wrappedLines_text)
@@ -434,8 +579,9 @@ displayLinesWithAlignment alignment width tag cursorTag (TextZipper lb b a la) =
434 -- * spans that are on earlier display lines (though on the same logical line), and
579 -- * spans that are on earlier display lines (though on the same logical line), and
435 -- * spans that are on the same display line
580 -- * spans that are on the same display line
436
581
582+ -- TODO cursor goes here if align right
437 (spansCurrentBefore, spansCurLineBefore) = fromMaybe ([], []) $
583 (spansCurrentBefore, spansCurLineBefore) = fromMaybe ([], []) $
438− initLast $ map ((:[]) . Span tag) $ _wrappedLines_text <$> (wrapWithOffsetAndAlignment alignment width 0 b)
584+ initLast $ map ((:[]) . Span tag) $ fmap _wrappedLines_text $ (wrapWithOffsetAndAlignment alignment width 0 b)
439 -- Calculate the number of columns on the cursor's display line before the cursor
585 -- Calculate the number of columns on the cursor's display line before the cursor
440 curLineOffset = spansWidth spansCurLineBefore
586 curLineOffset = spansWidth spansCurLineBefore
441 -- Check whether the spans on the current display line are long enough that
587 -- Check whether the spans on the current display line are long enough that
@@ -449,29 +595,17 @@ displayLinesWithAlignment alignment width tag cursorTag (TextZipper lb b a la) =
449 -- * spans that are on the same display line, and
595 -- * spans that are on the same display line, and
450 -- * spans that are on later display lines (though on the same logical line)
596 -- * spans that are on later display lines (though on the same logical line)
451
597
598+ -- TODO do not show cursor if align right
452 (spansCurLineAfter, spansCurrentAfter) = fromMaybe ([], []) $
599 (spansCurLineAfter, spansCurrentAfter) = fromMaybe ([], []) $
453 headTail $ case T.uncons a of
600 headTail $ case T.uncons a of
454 Nothing -> [[Span cursorTag " "]]
601 Nothing -> [[Span cursorTag " "]]
455 Just (c, rest) ->
602 Just (c, rest) ->
456 let o = if cursorAfterEOL then cursorCharWidth else curLineOffset + cursorCharWidth
603 let o = if cursorAfterEOL then cursorCharWidth else curLineOffset + cursorCharWidth
457 cursor = Span cursorTag (T.singleton c)
604 cursor = Span cursorTag (T.singleton c)
458− in case map ((:[]) . Span tag) $ _wrappedLines_text <$> (wrapWithOffsetAndAlignment alignment width o rest) of
605+ in case map ((:[]) . Span tag) $ fmap _wrappedLines_text $ (wrapWithOffsetAndAlignment alignment width o rest) of
459 [] -> [[cursor]]
606 [] -> [[cursor]]
460 (l:ls) -> (cursor : l) : ls
607 (l:ls) -> (cursor : l) : ls
461
608
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
609 cursorY = sum
476 [ length spansBefore
610 [ length spansBefore
477 , length spansCurrentBefore
611 , length spansCurrentBefore
@@ -480,16 +614,18 @@ displayLinesWithAlignment alignment width tag cursorTag (TextZipper lb b a la) =
480 -- a little silly to convert back to text but whatever, it works
614 -- 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)
615 cursorX = if cursorAfterEOL then 0 else textWidth (mconcat $ fmap (\(Span _ t) -> t) spansCurLineBefore)
482
616
483− in DisplayLines
617+ in DisplayLinesWithAlignment
484− { _displayLines_spans = concat
618+ { _displayLinesWithAlignment_spans = concat
485 [ spansBefore
619 [ spansBefore
486 , spansCurrentBefore
620 , spansCurrentBefore
487− , curLineSpan
621+ , if cursorAfterEOL
622+ then [ spansCurLineBefore, spansCurLineAfter ]
623+ else [ spansCurLineBefore <> spansCurLineAfter ]
488 , spansCurrentAfter
624 , spansCurrentAfter
489 , spansAfter
625 , spansAfter
490 ]
626 ]
491− , _displayLines_offsetMap = offsets
627+ , _displayLinesWithAlignment_offsetMap = offsets
492− , _displayLines_cursorPos = (cursorX, cursorY)
628+ , _displayLinesWithAlignment_cursorPos = (cursorX, cursorY)
493 }
629 }
494 where
630 where
495 initLast :: [a] -> Maybe ([a], a)
631 initLast :: [a] -> Maybe ([a], a)
@@ -508,38 +644,15 @@ displayLinesWithAlignment alignment width tag cursorTag (TextZipper lb b a la) =
508-- by the given display line coordinates, using the provided 'DisplayLinesWithAlignment'
644-- 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
645-- information. If the x coordinate is beyond the end of a line, the cursor is
510-- moved to the end of the line.
646-- moved to the end of the line.
511−goToDisplayLinePosition :: Int -> Int -> DisplayLines tag -> TextZipper -> TextZipper
647+goToDisplayLineWithAlignmentPosition :: Int -> Int -> DisplayLinesWithAlignment tag -> TextZipper -> TextZipper
512−goToDisplayLinePosition x y dl tz =
648+goToDisplayLineWithAlignmentPosition x y dl tz =
513− let offset = Map.lookup y $ _displayLines_offsetMap dl
649+ let offset = Map.lookup y $ _displayLinesWithAlignment_offsetMap dl
514 in case offset of
650 in case offset of
515 Nothing -> tz
651 Nothing -> tz
516 Just (alignOff,o) ->
652 Just (alignOff,o) ->
517 let
653 let
518 trueX = max 0 (x - alignOff)
654 trueX = max 0 (x - alignOff)
519− moveRight = case drop y $ _displayLines_spans dl of
655+ moveRight = case drop y $ _displayLinesWithAlignment_spans dl of
520 [] -> 0
656 [] -> 0
521 (s:_) -> charIndexAt trueX . stream . mconcat . fmap (\(Span _ t) -> t) $ s
657 (s:_) -> charIndexAt trueX . stream . mconcat . fmap (\(Span _ t) -> t) $ s
522 in rightN (o + moveRight) $ top tz
658 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+4−4
@@ -445,9 +445,9 @@ runImageWriter
445 -> m (a, Behavior t [Image])
445 -> m (a, Behavior t [Image])
446runImageWriter = runBehaviorWriterT . unImageWriter
446runImageWriter = runBehaviorWriterT . unImageWriter
447
447
448−-- * Theming
448+-- * theming
449
449
450−-- | A class for things that can be visually styled
450+-- | A class for things that can dynamically gain and lose focus
451class (Reflex t, Monad m) => HasTheme t m | m -> t where
451class (Reflex t, Monad m) => HasTheme t m | m -> t where
452 theme :: m (Behavior t V.Attr)
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)
453 default theme :: (f m' ~ m, Monad m', MonadTrans f, HasTheme t m') => m (Behavior t V.Attr)
@@ -466,7 +466,7 @@ instance HasTheme t m => HasTheme t (ImageWriter t m)
466instance HasTheme t m => HasTheme t (DisplayRegion t m)
466instance HasTheme t m => HasTheme t (DisplayRegion t m)
467instance HasTheme t m => HasTheme t (FocusReader t m)
467instance HasTheme t m => HasTheme t (FocusReader t m)
468
468
469−-- | A widget that has access to theme information
469+-- | A widget that has access to information about whether it is focused
470newtype ThemeReader t m a = ThemeReader
470newtype ThemeReader t m a = ThemeReader
471 { unThemeReader :: ReaderT (Behavior t V.Attr) m a }
471 { unThemeReader :: ReaderT (Behavior t V.Attr) m a }
472 deriving
472 deriving
@@ -505,7 +505,7 @@ instance MFunctor (ThemeReader t) where
505
505
506instance MonadNodeId m => MonadNodeId (ThemeReader t m)
506instance MonadNodeId m => MonadNodeId (ThemeReader t m)
507
507
508−-- | Run a 'ThemeReader' action with the given focus value
508+-- | Run a 'FocusReader' action with the given focus value
509runThemeReader
509runThemeReader
510 :: (Reflex t, Monad m)
510 :: (Reflex t, Monad m)
511 => Behavior t V.Attr
511 => Behavior t V.Attr
src/Reflex/Vty/Widget/Input.hs−1
@@ -116,7 +116,6 @@ checkboxStyleTick = CheckboxStyle
116-- | Configuration options for a checkbox
116-- | Configuration options for a checkbox
117data CheckboxConfig t = CheckboxConfig
117data CheckboxConfig t = CheckboxConfig
118 { _checkboxConfig_checkboxStyle :: Behavior t CheckboxStyle
118 { _checkboxConfig_checkboxStyle :: Behavior t CheckboxStyle
119− -- TODO DELETE and use HasTheme instead
120 , _checkboxConfig_attributes :: Behavior t V.Attr
119 , _checkboxConfig_attributes :: Behavior t V.Attr
121 , _checkboxConfig_setValue :: Event t Bool
120 , _checkboxConfig_setValue :: Event t Bool
122 }
121 }
src/Reflex/Vty/Widget/Input/Text.hs+2−1
@@ -74,7 +74,8 @@ textInput cfg = do
74 attrDyn <- holdDyn attr0 $ pushAlways (\_ -> sample bt) (updated rowInputDyn)
74 attrDyn <- holdDyn attr0 $ pushAlways (\_ -> sample bt) (updated rowInputDyn)
75 let rows = ffor2 attrDyn rowInputDyn toDisplayLines
75 let rows = ffor2 attrDyn rowInputDyn toDisplayLines
76 img = images . _displayLines_spans <$> rows
76 img = images . _displayLines_spans <$> rows
77− y <- holdUniqDyn $ fmap snd _displayLines_cursorPos <$> rows
77+
78+ y <- holdUniqDyn $ _displayLines_cursorY <$> rows
78 let newScrollTop :: Int -> (Int, Int) -> Int
79 let newScrollTop :: Int -> (Int, Int) -> Int
79 newScrollTop st (h, cursorY)
80 newScrollTop st (h, cursorY)
80 | cursorY < st = cursorY
81 | cursorY < st = cursorY
src/Reflex/Vty/Widget/Layout.hs−4
@@ -133,8 +133,6 @@ instance (HasImageWriter t m, MonadFix m) => HasImageWriter t (Focus t m) where
133
133
134instance (HasFocusReader t m, Monad m) => HasFocusReader t (Focus t m)
134instance (HasFocusReader t m, Monad m) => HasFocusReader t (Focus t m)
135
135
136−instance (HasTheme t m, Monad m) => HasTheme t (Focus t m)
137−
138instance (Reflex t, MonadFix m, MonadNodeId m) => HasFocus t (Focus t m) where
136instance (Reflex t, MonadFix m, MonadNodeId m) => HasFocus t (Focus t m) where
139 makeFocus = do
137 makeFocus = do
140 fid <- FocusId <$> lift getNextNodeId
138 fid <- FocusId <$> lift getNextNodeId
@@ -432,8 +430,6 @@ instance (HasDisplayRegion t m, HasImageWriter t m, MonadFix m) => HasImageWrite
432
430
433instance (HasFocusReader t m, Monad m) => HasFocusReader t (Layout t m)
431instance (HasFocusReader t m, Monad m) => HasFocusReader t (Layout t m)
434
432
435−instance (HasTheme t m, Monad m) => HasTheme t (Layout t m)
436−
437instance (Monad m, MonadNodeId m, Reflex t, MonadFix m) => HasLayout t (Layout t m) where
433instance (Monad m, MonadNodeId m, Reflex t, MonadFix m) => HasLayout t (Layout t m) where
438 axis o c (Layout x) = Layout $ do
434 axis o c (Layout x) = Layout $ do
439 nodeId <- getNextNodeId
435 nodeId <- getNextNodeId
test/Data/Text/ZipperSpec.hs+64−83
@@ -9,96 +9,77 @@ import Prelude
9import Test.Hspec
9import Test.Hspec
10
10
11import qualified Data.Map as Map
11import qualified Data.Map as Map
12−import qualified Data.Text as T
12+import Data.Text
13−import Control.Monad
14
13
15import Data.Text.Zipper
14import Data.Text.Zipper
16
15
17
16
18someSentence = "12345 1234 12"
17someSentence = "12345 1234 12"
19
18
20−splitSentenceAtDisplayWidth :: Int -> T.Text -> [(T.Text, Bool)]
19+splitSentenceAtDisplayWidth :: Int -> Text -> [(Text, Bool)]
21splitSentenceAtDisplayWidth w t = splitWordsAtDisplayWidth w (wordsWithWhitespace t)
20splitSentenceAtDisplayWidth w t = splitWordsAtDisplayWidth w (wordsWithWhitespace t)
22
21
23spec :: Spec
22spec :: Spec
24−spec =
23+spec = do
25 describe "Zipper" $ do
24 describe "Zipper" $ do
26− it "wordsWithWhitespace" $ do
25+ it "wordsWithWhitespace" $ do
27− wordsWithWhitespace "" `shouldBe` []
26+ wordsWithWhitespace "" `shouldBe` []
28− wordsWithWhitespace "😱😱😱" `shouldBe` ["😱😱😱"]
27+ wordsWithWhitespace "😱😱😱" `shouldBe` ["😱😱😱"]
29− wordsWithWhitespace "abcd efgf f" `shouldBe` ["abcd ","efgf ", "f"]
28+ wordsWithWhitespace "abcd efgf f" `shouldBe` ["abcd ","efgf ", "f"]
30− wordsWithWhitespace "aoeu " `shouldBe` ["aoeu "]
29+ wordsWithWhitespace "aoeu " `shouldBe` ["aoeu "]
31− it "splitWordsAtDisplayWidth" $ do
30+ it "splitWordsAtDisplayWidth" $ do
32− fmap fst (splitSentenceAtDisplayWidth 5 "123456") `shouldBe` ["12345","6"]
31+ fmap fst (splitSentenceAtDisplayWidth 5 "123456") `shouldBe` ["12345","6"]
33− fmap fst (splitSentenceAtDisplayWidth 5 "12345 6") `shouldBe` ["12345","6"]
32+ fmap fst (splitSentenceAtDisplayWidth 5 "12345 6") `shouldBe` ["12345","6"]
34− fmap fst (splitSentenceAtDisplayWidth 5 "1234 56") `shouldBe` ["1234","56"]
33+ fmap fst (splitSentenceAtDisplayWidth 5 "1234 56") `shouldBe` ["1234","56"]
35− fmap fst (splitSentenceAtDisplayWidth 5 "12345678912345") `shouldBe` ["12345","67891","2345"]
34+ fmap fst (splitSentenceAtDisplayWidth 5 "12345678912345") `shouldBe` ["12345","67891","2345"]
36− fmap fst (splitSentenceAtDisplayWidth 5 "1234 56") `shouldBe` ["1234 "," 56"]
35+ 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"]
36+ 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
37+ it "wrapWithOffsetAndAlignment" $ do
39− wrapWithOffsetAndAlignment TextAlignment_Left 5 0 someSentence `shouldBe` [(WrappedLine "12345" True 0), (WrappedLine "1234" True 0), (WrappedLine "12" False 0)]
38+ 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)]
39+ 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)]
40+ 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
41+ it "eolSpacesToLogicalLines" $ do
47− eolSpacesToLogicalLines
42+ eolSpacesToLogicalLines
48− [
43+ [
49− [ (WrappedLine "😱" True 1), (WrappedLine "😱" False 2), (WrappedLine "😱" False 3) ]
44+ [ (WrappedLine "😱" True 1), (WrappedLine "😱" False 2), (WrappedLine "😱" False 3) ]
50− , [ (WrappedLine "aa" True 1), (WrappedLine "aa" True 2), (WrappedLine "aa" False 3) ]
45+ , [ (WrappedLine "aa" True 1), (WrappedLine "aa" True 2), (WrappedLine "aa" False 3) ]
51− ]
46+ ]
52− `shouldBe`
47+ `shouldBe`
53− [
48+ [
54− [ ("😱",1) ]
49+ [ ("😱",1) ]
55− , [ ("😱",2), ("😱",3) ]
50+ , [ ("😱",2), ("😱",3) ]
56− , [ ("aa",1) ]
51+ , [ ("aa",1) ]
57− , [ ("aa",2) ]
52+ , [ ("aa",2) ]
58− , [ ("aa",3) ]
53+ , [ ("aa",3) ]
59− ]
54+ ]
60− it "offsetMapWithAlignmentInternal" $ do
55+ it "offsetMapWithAlignmentInternal" $ do
61− offsetMapWithAlignmentInternal
56+ offsetMapWithAlignmentInternal
62− [
57+ [
63− [ (WrappedLine "😱" True 1), (WrappedLine "😱" False 2), (WrappedLine "😱" False 3) ]
58+ [ (WrappedLine "😱" True 1), (WrappedLine "😱" False 2), (WrappedLine "😱" False 3) ]
64− , [ (WrappedLine "aa" True 1), (WrappedLine "aa" True 2), (WrappedLine "aa" False 3) ]
59+ , [ (WrappedLine "aa" True 1), (WrappedLine "aa" True 2), (WrappedLine "aa" False 3) ]
65− ]
60+ ]
66− `shouldBe`
61+ `shouldBe`
67− Map.fromList
62+ Map.fromList
68− [ (0, (1,0))
63+ [ (0, (1,0))
69− , (1, (2,2)) -- jump by 1 for char and 1 for space
64+ , (1, (2,2)) -- jump by 1 for char and 1 for space
70− , (2, (3,3)) -- jump by 1 for char
65+ , (2, (3,3)) -- jump by 1 for char
71− , (3, (1,5)) -- jump by 1 for char and 1 for newline
66+ , (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
67+ , (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
68+ , (5, (3,11)) -- jump by 2 for char and 1 for space
74− ]
69+ ]
75− it "displayLines - cursorPos" $ do
70+ it "displayLinesWithAlignment - cursorPos" $ do
76− let
71+ let
77− dl0 = displayLinesWithAlignment TextAlignment_Right 10 () () (fromText "")
72+ dl0 = displayLinesWithAlignment TextAlignment_Right 10 () () (fromText "")
78− dl1 = displayLinesWithAlignment TextAlignment_Right 10 () () (fromText "aoeu")
73+ dl1 = displayLinesWithAlignment TextAlignment_Right 10 () () (fromText "aoeu")
79− dl2 = displayLinesWithAlignment TextAlignment_Right 10 () () (fromText "aoeu\n")
74+ dl2 = displayLinesWithAlignment TextAlignment_Right 10 () () (fromText "aoeu\n")
80− dl3 = displayLinesWithAlignment TextAlignment_Right 10 () () (fromText "0123456789")
75+ dl3 = displayLinesWithAlignment TextAlignment_Right 10 () () (fromText "0123456789")
81− dl4 = displayLinesWithAlignment TextAlignment_Right 10 () () (insertChar 'a' $ fromText "aoeu")
76+ dl4 = displayLinesWithAlignment TextAlignment_Right 10 () () (insertChar 'a' $ fromText "aoeu")
82− dl5 = displayLinesWithAlignment TextAlignment_Right 10 () () (left $ insertChar 'a' $ fromText "aoeu")
77+ dl5 = displayLinesWithAlignment TextAlignment_Right 10 () () (left $ insertChar 'a' $ fromText "aoeu")
83− dl6 = displayLinesWithAlignment TextAlignment_Right 10 () () (deleteLeft $ insertChar 'a' $ fromText "aoeu")
78+ dl6 = displayLinesWithAlignment TextAlignment_Right 10 () () (deleteLeft $ insertChar 'a' $ fromText "aoeu")
84− _displayLines_cursorPos dl0 `shouldBe` (0,0)
79+ _displayLinesWithAlignment_cursorPos dl0 `shouldBe` (0,0)
85− _displayLines_cursorPos dl1 `shouldBe` (4,0)
80+ _displayLinesWithAlignment_cursorPos dl1 `shouldBe` (4,0)
86− _displayLines_cursorPos dl2 `shouldBe` (0,1)
81+ _displayLinesWithAlignment_cursorPos dl2 `shouldBe` (0,1)
87− _displayLines_cursorPos dl3 `shouldBe` (0,1)
82+ _displayLinesWithAlignment_cursorPos dl3 `shouldBe` (0,1)
88− _displayLines_cursorPos dl4 `shouldBe` (5,0)
83+ _displayLinesWithAlignment_cursorPos dl4 `shouldBe` (5,0)
89− _displayLines_cursorPos dl5 `shouldBe` (4,0)
84+ _displayLinesWithAlignment_cursorPos dl5 `shouldBe` (4,0)
90− _displayLines_cursorPos dl6 `shouldBe` (4,0)
85+ _displayLinesWithAlignment_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