on develop help sign in
20 files changed+1772−1076
since splitsnapshots
unifiedsplit
.github/workflows/haskell.yml+12−10 .travis.yml−112 ChangeLog.md+90−54 reflex-vty.cabal+29−8 src-bin/Example/CPU.hs+231 src-bin/example.hs+37−21 src/Control/Monad/NodeId.hs+3−1 src/Data/Text/Zipper.hs+280−157 src/Reflex/Class/Switchable.hs−27 src/Reflex/Vty.hs+6 src/Reflex/Vty/Widget.hs+457−616 src/Reflex/Vty/Widget/Box.hs+124 src/Reflex/Vty/Widget/Input.hs+18−8 src/Reflex/Vty/Widget/Input/Mouse.hs+106 src/Reflex/Vty/Widget/Input/Text.hs+17−12 src/Reflex/Vty/Widget/Layout.hs+45−50 src/Reflex/Vty/Widget/Split.hs+92 src/Reflex/Vty/Widget/Text.hs+106 test/Data/Text/ZipperSpec.hs+104 test/Spec.hs+15
.github/workflows/haskell.yml+12−10
@@ -1,33 +1,33 @@
1−name: Haskell CI
1+name: github-action
22
33on: [push, pull_request]
44
55jobs:
66 build:
7−
8− runs-on: ${{ matrix.os }}
97 strategy:
108 matrix:
11− ghc: ['8.4.4', '8.6.5', '8.8.4']
9+ ghc: ['8.4.4', '8.6.5', '8.8.4', '8.10.2']
1210 os: ['ubuntu-latest', 'macos-latest']
11+ runs-on: ${{ matrix.os }}
12+
13+ name: GHC ${{ matrix.ghc }} on ${{ matrix.os }}
1314 steps:
1415 - uses: actions/checkout@v2
1516 - uses: actions/setup-haskell@v1
1617 with:
1718 ghc-version: ${{ matrix.ghc }}
18− cabal-version: '3.0'
19−
2019 - name: Cache
2120 uses: actions/cache@v1
2221 env:
2322 cache-name: cache-cabal
2423 with:
2524 path: ~/.cabal
26− key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/*.cabal') }}-${{ hashFiles('**/cabal.project') }}
25+ key: ${{ runner.os }}-${{ matrix.ghc }}-build-${{ env.cache-name }}-${{ hashFiles('**/*.cabal') }}-${{ hashFiles('**/cabal.project') }}
2726 restore-keys: |
28− ${{ runner.os }}-build-${{ env.cache-name }}-
29− ${{ runner.os }}-build-
30− ${{ runner.os }}-
27+ ${{ runner.os }}-${{ matrix.ghc }}-build-${{ env.cache-name }}-
28+ ${{ runner.os }}-${{ matrix.ghc }}-build-
29+ ${{ runner.os }}-${{ matrix.ghc }}-
30+ ${{ runner.os }}
3131
3232 - name: Install dependencies
3333 run: |
@@ -37,3 +37,5 @@ jobs:
3737 run: cabal build --enable-tests --enable-benchmarks all
3838 - name: Run tests
3939 run: cabal test all
40+ - name: Build Docs
41+ run: cabal haddock
.travis.ymldeleted−112
@@ -1,112 +0,0 @@
1−# This Travis job script has been generated by a script via
2−#
3−# haskell-ci 'reflex-vty.cabal' '--output' '.travis.yml'
4−#
5−# For more information, see https://github.com/haskell-CI/haskell-ci
6−#
7−# version: 0.3
8−#
9−language: c
10−dist: xenial
11−
12−git:
13− submodules: false # whether to recursively clone submodules
14−
15−cache:
16− directories:
17− - $HOME/.cabal/packages
18− - $HOME/.cabal/store
19−
20−before_cache:
21− - rm -fv $CABALHOME/packages/hackage.haskell.org/build-reports.log
22− # remove files that are regenerated by 'cabal update'
23− - rm -fv $CABALHOME/packages/hackage.haskell.org/00-index.*
24− - rm -fv $CABALHOME/packages/hackage.haskell.org/*.json
25− - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.cache
26− - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar
27− - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar.idx
28−
29− - rm -rfv $CABALHOME/packages/head.hackage
30−
31−matrix:
32− include:
33− - compiler: "ghc-8.8.3"
34− addons: {apt: {packages: [ghc-ppa-tools,cabal-install-3.0,ghc-8.8.3], sources: [hvr-ghc]}}
35− - compiler: "ghc-8.6.5"
36− addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-8.6.5], sources: [hvr-ghc]}}
37− - compiler: "ghc-8.4.4"
38− addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-8.4.4], sources: [hvr-ghc]}}
39−
40−
41−before_install:
42− - HC=/opt/ghc/bin/${CC}
43− - HCPKG=${HC/ghc/ghc-pkg}
44− - unset CC
45− - CABAL=/opt/ghc/bin/cabal
46− - CABALHOME=$HOME/.cabal
47− - export PATH="$CABALHOME/bin:$PATH"
48− - ROOTDIR=$(pwd)
49− - HCNUMVER=$(( $(${HC} --numeric-version|sed -E 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\1 * 10000 + \2 * 100 + \3/') ))
50− - echo $HCNUMVER
51−
52−install:
53− - ${CABAL} --version
54− - echo "$(${HC} --version) [$(${HC} --print-project-git-commit-id 2> /dev/null || echo '?')]"
55− - TEST=--enable-tests
56− - BENCH=--enable-benchmarks
57− - GHCHEAD=${GHCHEAD-false}
58− - travis_retry ${CABAL} update -v
59− - sed -i.bak 's/^jobs:/-- jobs:/' $CABALHOME/config
60− - rm -fv cabal.project cabal.project.local
61− - grep -Ev -- '^\s*--' $CABALHOME/config | grep -Ev '^\s*$'
62− - rm -f cabal.project
63− - touch cabal.project
64− - "printf 'packages: \".\"\\n' >> cabal.project"
65− - "printf 'write-ghc-environment-files: always\\n' >> cabal.project"
66− - touch cabal.project.local
67− - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | grep -vE -- '^(reflex-vty)$' | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done"
68− - cat cabal.project || true
69− - cat cabal.project.local || true
70− - if [ -f "./configure.ac" ]; then (cd "." && autoreconf -i); fi
71− - rm -f cabal.project.freeze
72− - ${CABAL} new-freeze -w ${HC} ${TEST} ${BENCH} --project-file="cabal.project" --dry
73− - "cat \"cabal.project.freeze\" | sed -E 's/^(constraints: *| *)//' | sed 's/any.//'"
74− - rm "cabal.project.freeze"
75− - ${CABAL} new-build -w ${HC} ${TEST} ${BENCH} --project-file="cabal.project" --dep -j2 all
76− - ${CABAL} new-build -w ${HC} --disable-tests --disable-benchmarks --project-file="cabal.project" --dep -j2 all
77− - rm -rf .ghc.environment.* "."/dist
78− - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)
79−
80−# Here starts the actual work to be performed for the package under test;
81−# any command which exits with a non-zero exit code causes the build to fail.
82−script:
83− # test that source-distributions can be generated
84− - ${CABAL} new-sdist all
85− - mv dist-newstyle/sdist/*.tar.gz ${DISTDIR}/
86− - cd ${DISTDIR} || false
87− - find . -maxdepth 1 -name '*.tar.gz' -exec tar -xvf '{}' \;
88− - rm -f cabal.project
89− - touch cabal.project
90− - "printf 'packages: \"reflex-vty-*/*.cabal\"\\n' >> cabal.project"
91− - "printf 'write-ghc-environment-files: always\\n' >> cabal.project"
92− - touch cabal.project.local
93− - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | grep -vE -- '^(reflex-vty)$' | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done"
94− - cat cabal.project || true
95− - cat cabal.project.local || true
96− # this builds all libraries and executables (without tests/benchmarks)
97− - ${CABAL} new-build -w ${HC} --disable-tests --disable-benchmarks all
98−
99− # build & run tests, build benchmarks
100− - ${CABAL} new-build -w ${HC} ${TEST} ${BENCH} all
101−
102− # cabal check
103− - (cd reflex-vty-* && ${CABAL} check)
104−
105− # haddock
106− - ${CABAL} new-haddock -w ${HC} ${TEST} ${BENCH} all
107−
108− # Build without installed constraints for packages in global-db
109− - rm -f cabal.project.local; ${CABAL} new-build -w ${HC} --disable-tests --disable-benchmarks all
110−
111−# REGENDATA ["reflex-vty.cabal","--output",".travis.yml"]
112−# EOF
ChangeLog.md+90−54
@@ -1,60 +1,96 @@
11# Revision history for reflex-vty
22
33## 0.2.0.0
4−* Remove text-icu dependency and switch to `wcwidth` from vty package to compute character width in `Data.Text.Zipper`.
5−* `goToDisplayLinePosition` in `Data.Text.Zipper` correctly accounts for character width now.
6−* [#37](https://github.com/reflex-frp/reflex-vty/issues/37) `Layout` should support focus changes through nested layouts (thanks @pdlla for getting this started).
7−* _Breaking Change_: Layout and focus have been substantially refactored:
8− * Added `MonadFocus` to produce focusable elements, and manage focus state. See the "Focus" section of the Reflex.Vty.Widget.Layout module documentation.
9− * `Layout` no longer has any focus-tracking responsibility. See the "Layout" section of the Reflex.Vty.Widget.Layout module documentation.
10− * `tile` no longer takes a configuration record and no longer requires that its child widget return a focus request event. Focus requests are instead handled using calls to `requestFocus` in the child widget.
11− * Calls to `fixed` and `stretch` must now be replaced with `tile . fixed` and `tile . stretch`
12− * `stretch` now takes a minimum size argument
13− * Added `flex` which is equivalent to `stretch 0`
14− * `tabNavigation` no longer returns an `Event`. Instead it calls `requestFocus` directly with the appropriate `Refocus_Shift` value.
15− * Added `axis` (in `MonadLayout`), a lower-level primitive which is used to implement `row` and `col`.
16− * Added `region` (in `MonadLayout`), which is used to claim screen real estate and used to implement `tile` and `grout`
17− * Added `grout`, a container element that is not itself focusable (though its children can be)
18−* Add `HasVtyWidgetCtx` for `pane`-like sub-widget container behavior
19−* Add default instances for `HasVtyInput`, `HasFocus`, and `ImageWriter`
20−* _Breaking Change_: Remove `DynRegion` and `currentRegion`. Use `Dynamic t Region` and `current` instead. This also changes the type of `pane`'s argument.
21−* _Breaking Change_: The following functions are no longer specialized to `VtyWidget`:
22− * `pane`: Now requires `HasVtyWidgetCtx` instead
23− * `drag`: Now requires `HasVtyInput` instead
24− * `mouseDown`: Now requires `HasVtyInput` instead
25− * `mouseUp`: Now requires `HasVtyInput` instead
26− * `mouseScroll`: Now requires `HasVtyInput` instead
27− * `key`: Now requires `HasVtyInput` instead
28− * `keys`: Now requires `HasVtyInput` instead
29− * `keyCombo`: Now requires `HasVtyInput` instead
30− * `keyCombos`: Now requires `HasVtyInput` instead
31− * `splitV`: Now requires `HasVtyWidgetCtx` and `HasDisplaySize` instead
32− * `splitH`: Now requires `HasVtyWidgetCtx` and `HasDisplaySize` instead
33− * `splitVDrag`: Now requires `HasVtyWidgetCtx`, `HasVtyInput`, and `HasDisplaySize` instead
34− * `fill`: Now requires `ImageWriter` and `HasDisplaySize` instead
35− * `boxTitle`: Now requires `HasVtyWidgetCtx`, `ImageWriter`, and `HasDisplaySize` instead
36− * `box`: Now requires `HasVtyWidgetCtx`, `ImageWriter`, and `HasDisplaySize` instead
37− * `boxStatic`: Now requires `HasVtyWidgetCtx`, `ImageWriter`, and `HasDisplaySize` instead
38− * `richText`: Now requires `ImageWriter`, and `HasDisplaySize` instead
39− * `scrollableText`: Now requires `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
40− * `blank`: Now required `Monad` instead
41− * `button`: Now requires `HasFocus`, `HasVtyWidgetCtx`, `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
42− * `textButton`: Now requires `HasFocus`, `HasVtyWidgetCtx`, `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
43− * `textButtonStatic`: Now requires `HasFocus`, `HasVtyWidgetCtx`, `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
44− * `link`: Now requires `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
45− * `checkbox`: Now requires `HasFocus`, `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
46− * `textInput`: Now requires `HasFocus`, `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
47− * `multilineTextInput`: Now requires `HasFocus`, `HasVtyInput`, `ImageWriter`, and `HasDisplaySize` instead
48− * `textInputTile`: Now requires `HasVtyWidgetCtx`, `HasVtyInput`, `MonadLayout`, and `MonadFocus` instead
49−* _Breaking Change_: `CheckboxConfig` now has a field taking an `Event` to set the value of the checkbox.
50−* _Breaking Change_: `checkbox` now accepts keyboard input (spacebar to check and uncheck) and is displayed in bold when focused.
51−* _Breaking Change_: `HasVtyInput` now has a method `localInput` for filtering the input a child widget may receive
52−* Add `anyChildFocused`, which provides information about whether subwidgets are focused
53−* Add `filterKeys`, which is the same as `localInput` but only cares about keyboard events
54−* _Breaking Change_: `ImageWriter` now has a method `mapImages` for transforming the images emitted by a child widget
55−* Add `hoistRunLayout` to apply a transformation to the context of a `Layout` action and run that action
56−* Add an `MFunctor` instance for `VtyWidget`
57−* Bug: fix distribution of available space when it cannot be evenly distributed. Previously, all leftover space would be allocated to the first stretchable widget.
4+
5+* _Module Reorganization_: The following modules have been added (and are all re-exported by Reflex.Vty):
6+ * Reflex.Vty.Widget.Box for all the box functions and datatypes
7+ * Reflex.Vty.Widget.Input.Mouse for clicking, dragging, and scrolling
8+ * Reflex.Vty.Widget.Split contains `splitV`, `splitH`, etc
9+ * Reflex.Vty.Widget.Text contains text rendering functions like `text` and `display`
10+* _Bugfixes_:
11+ * Remove text-icu dependency and switch to `wcwidth` from vty package to compute character width in `Data.Text.Zipper`.
12+ * `goToDisplayLinePosition` in `Data.Text.Zipper` correctly accounts for character width now.
13+ * [#37](https://github.com/reflex-frp/reflex-vty/issues/37) `Layout` should support focus changes through nested layouts (thanks @pdlla for getting this started -- see entry on Layout and Focus below).
14+ * Fix distribution of available space when it cannot be evenly distributed. Previously, all leftover space would be allocated to the first stretchable widget.
15+* _Breaking Changes_:
16+ * Layout and focus have been substantially refactored to fix [#37](https://github.com/reflex-frp/reflex-vty/issues/37) and support a wider variety of layouts and focus switching requirements.
17+ * Added a new `HasFocus` class (the old one is now `HasFocusReader`) to produce focusable elements, and manage focus state. See the "Focus" section of the Reflex.Vty.Widget.Layout module documentation.
18+ * `Layout` no longer has any focus-tracking responsibility. See the "Layout" section of the Reflex.Vty.Widget.Layout module documentation.
19+ * `tile` no longer takes a configuration record and no longer requires that its child widget return a focus request event. Focus requests are instead handled using calls to `requestFocus` in the child widget.
20+ * Calls to `fixed` and `stretch` must now be replaced with `tile . fixed` and `tile . stretch`
21+ * `stretch` now takes a minimum size argument
22+ * Added `flex` which is equivalent to `stretch 0`
23+ * `tabNavigation` no longer returns an `Event`. Instead it calls `requestFocus` directly with the appropriate `Refocus_Shift` value.
24+ * Added `axis` (in `HasLayout`), a lower-level primitive which is used to implement `row` and `col`.
25+ * Added `region` (in `HasLayout`), which is used to claim screen real estate and used to implement `tile` and `grout`
26+ * Added `grout`, a container element that is not itself focusable (though its children can be)
27+ * Removed `VtyWidget` and replaced it with a number of separate classes and monad transformers
28+ * Replace `HasDisplaySize` with `HasDisplayRegion` which carries around a region instead of just a width and height. `displayWidth` and `displayHeight` are now functions implemented in terms of `askRegion` instead of class methods.
29+ * Add a `DisplayRegion` monad transformer
30+ * Rename `ImageWriter` to `HasImageWriter`
31+ * Introduce an `ImageWriter` monad transformer
32+ * Rename `HasFocus` to `HasFocusReader`
33+ * Introduce a `FocusReader` monad transformer
34+ * Replace `HasVtyInput` with `HasInput`
35+ * Introduce an `Input` monad transformer
36+ * 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.
39+ * `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.
41+ * `HasInput` (formerly `HasVtyInput`) now has a method `localInput` for filtering the input a child widget may receive
42+ * `HasImageWriter` now has a method `mapImages` for transforming the images emitted by a child widget
43+ * `boxTitle` now takes a `Behavior t Text` as its title, instead of a plain `Text`
44+ * `fill` now takes a `Behavior t Char` instead of a `Char`
45+ * The following functions are no longer specialized to `VtyWidget`:
46+ * `pane`: Now requires `HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m`
47+ * `drag`: Now requires `HasInput`
48+ * `mouseDown`: Now requires `HasInput`
49+ * `mouseUp`: Now requires `HasInput`
50+ * `mouseScroll`: Now requires `HasInput`
51+ * `key`: Now requires `HasInput`
52+ * `keys`: Now requires `HasInput`
53+ * `keyCombo`: Now requires `HasInput`
54+ * `keyCombos`: Now requires `HasInput`
55+ * `splitV`: Now requires `HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m`
56+ * `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`
58+ * `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`
62+ * `richText`: Now requires `HasImageWriter`, and `HasDisplayRegion`
63+ * `scrollableText`: Now requires `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
64+ * `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`
69+ * `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`
77+* _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
81+ * Add default instances for `HasInput`, `HasFocus`, and `HasImageWriter`
82+ * Export `withinImage` and add `imagesInRegion` to crop images to a region
83+ * Add `anyChildFocused`, which provides information about whether subwidgets are focused
84+ * Add `filterKeys`, which is the same as `localInput` but only cares about keyboard events
85+ * Add `hoistRunLayout` to apply a transformation to the context of a `Layout` action and run that action
86+ * Add various `MFunctor` instances
87+ * Add a CPU usage indicator to the example executable
88+
89+## 0.1.4.2
90+
91+* Wider bounds for GHC 8.10 support
92+
93+## 0.1.4.1
5894
5995## 0.1.4.1
6096* Migrate to new dependent-sum / dependent-map (after the "some" package split)
reflex-vty.cabal+29−8
@@ -12,27 +12,35 @@ maintainer: maintainer@obsidian.systems
1212copyright: 2020 Obsidian Systems LLC
1313category: FRP
1414build-type: Simple
15−cabal-version: >=1.18
15+cabal-version: 1.18
1616extra-source-files:
1717 README.md
1818 ChangeLog.md
1919extra-doc-files: doc/tasks.png
20−tested-with: GHC ==8.8.4 || ==8.6.5 || ==8.4.4
20+tested-with:
21+ GHC ==8.4.4 || ==8.6.5 || ==8.8.4 || ==8.10.2
22+
23+source-repository head
24+ type: git
25+ location: https://github.com/reflex-frp/reflex-vty
2126
2227library
2328 exposed-modules: Reflex.Vty
2429 , Reflex.Vty.Host
2530 , Reflex.Vty.Widget
31+ , Reflex.Vty.Widget.Box
2632 , Reflex.Vty.Widget.Input
33+ , Reflex.Vty.Widget.Input.Mouse
2734 , Reflex.Vty.Widget.Input.Text
2835 , Reflex.Vty.Widget.Layout
36+ , Reflex.Vty.Widget.Split
37+ , Reflex.Vty.Widget.Text
2938 , Data.Text.Zipper
30− , Reflex.Class.Switchable
3139 , Reflex.Spider.Orphans
3240 , Control.Monad.NodeId
3341 build-depends:
34− base >= 4.10.0 && < 4.14,
35− bimap >= 0.3.3 && < 0.4,
42+ base >= 4.10.0 && < 4.15,
43+ bimap >= 0.3.3 && < 0.5,
3644 containers >= 0.5.0 && < 0.7,
3745 mtl >= 2.2.2 && < 2.3,
3846 transformers >= 0.5.5 && < 0.6,
@@ -128,6 +136,19 @@ executable example
128136 TypeFamilies
129137
130138
131−source-repository head
132− type: git
133− location: https://github.com/reflex-frp/reflex-vty
139+test-suite reflex-vty-test
140+ hs-source-dirs: test
141+ main-is: Spec.hs
142+ type: exitcode-stdio-1.0
143+ ghc-options: -threaded
144+ other-modules:
145+ Data.Text.ZipperSpec
146+ build-depends:
147+ base,
148+ reflex,
149+ containers,
150+ reflex-vty,
151+ text,
152+ extra,
153+ hspec >= 2.7 && < 2.8
154+ default-language: Haskell2010
src-bin/Example/CPU.hsnew file+231
@@ -0,0 +1,231 @@
1+{-|
2+ Description: A CPU usage indicator
3+-}
4+module Example.CPU where
5+
6+import Control.Exception
7+import Control.Monad.Fix
8+import Control.Monad.IO.Class
9+import Data.Ratio
10+import Data.Time
11+import Data.Word
12+import qualified Data.Text as T
13+import qualified Graphics.Vty as V
14+import Text.Printf
15+
16+import Reflex
17+import Reflex.Vty
18+
19+-- | Each constructor represents a cpu statistic column as presented in @/proc/stat@
20+data CpuStat
21+ = CpuStat_User
22+ | CpuStat_Nice
23+ | CpuStat_System
24+ | CpuStat_Idle
25+ | CpuStat_Iowait
26+ | CpuStat_Irq
27+ | CpuStat_Softirq
28+ | CpuStat_Steal
29+ | CpuStat_Guest
30+ | CpuStat_GuestNice
31+ deriving (Show, Read, Eq, Ord, Enum, Bounded)
32+
33+-- | Read @/proc/stat@
34+getCpuStat :: IO (Maybe (CpuStat -> Word64))
35+getCpuStat = do
36+ s <- readFile "/proc/stat"
37+ _ <- evaluate $ length s -- Make readFile strict
38+ pure $ do
39+ cpuSummaryLine : _ <- pure $ lines s
40+ [user, nice, system, idle, iowait, irq, softirq, steal, guest, guestNice] <- pure $ map read $ words $ drop 4 cpuSummaryLine
41+ pure $ \case
42+ CpuStat_User -> user
43+ CpuStat_Nice -> nice
44+ CpuStat_System -> system
45+ CpuStat_Idle -> idle
46+ CpuStat_Iowait -> iowait
47+ CpuStat_Irq -> irq
48+ CpuStat_Softirq -> softirq
49+ CpuStat_Steal -> steal
50+ CpuStat_Guest -> guest
51+ CpuStat_GuestNice -> guestNice
52+
53+sumStats :: (CpuStat -> Word64) -> [CpuStat] -> Word64
54+sumStats get stats = sum $ get <$> stats
55+
56+-- | user + nice + system + irq + softirq + steal
57+nonIdleStats :: [CpuStat]
58+nonIdleStats =
59+ [ CpuStat_User
60+ , CpuStat_Nice
61+ , CpuStat_System
62+ , CpuStat_Irq
63+ , CpuStat_Softirq
64+ , CpuStat_Steal
65+ ]
66+
67+-- | idle + iowait
68+idleStats :: [CpuStat]
69+idleStats =
70+ [ CpuStat_Idle
71+ , CpuStat_Iowait
72+ ]
73+
74+-- | Draws the cpu usage percent as a live-updating bar graph. The output should look like:
75+--
76+-- > ╔══════ CPU Usage: 38% ══════╗
77+-- > ║ ║
78+-- > ║ ║
79+-- > ║ ║
80+-- > ║ ║
81+-- > ║ ║
82+-- > ║ ║
83+-- > ║█████████████████████████████║
84+-- > ║█████████████████████████████║
85+-- > ║█████████████████████████████║
86+-- > ║█████████████████████████████║
87+-- > ╚═════════════════════════════╝
88+--
89+cpuStats
90+ :: ( Reflex t
91+ , MonadFix m
92+ , MonadHold t m
93+ , MonadIO (Performable m)
94+ , MonadIO m
95+ , PerformEvent t m
96+ , PostBuild t m
97+ , TriggerEvent t m
98+ , HasDisplayRegion t m
99+ , HasImageWriter t m
100+ , HasLayout t m
101+ , HasFocus t m
102+ , HasInput t m
103+ , HasFocusReader t m, HasTheme t m
104+ )
105+ => m ()
106+cpuStats = do
107+ tick <- tickLossy 0.25 =<< liftIO getCurrentTime
108+ cpuStat :: Event t (Word64, Word64) <- fmap (fmapMaybe id) $
109+ performEvent $ ffor tick $ \_ -> do
110+ get <- liftIO getCpuStat
111+ pure $ case get of
112+ Nothing -> Nothing
113+ Just get' -> Just (sumStats get' nonIdleStats, sumStats get' idleStats)
114+ active <- foldDyn cpuPercentStep ((0, 0), 0) cpuStat
115+ let pct = fmap snd active
116+ chart pct
117+
118+chart
119+ :: ( MonadFix m
120+ , HasFocus t m
121+ , HasLayout t m
122+ , HasImageWriter t m
123+ , HasInput t m
124+ , HasDisplayRegion t m
125+ , HasFocusReader t m, HasTheme t m
126+ )
127+ => Dynamic t (Ratio Word64) -> m ()
128+chart pct = do
129+ let title = ffor pct $ \x -> mconcat
130+ [ " CPU Usage: "
131+ , T.pack (printf "%3d" $ (ceiling $ x * 100 :: Int))
132+ , "% "
133+ ]
134+ boxTitle (pure doubleBoxStyle) (current title) $ col $ do
135+ grout flex blank
136+ dh <- displayHeight
137+ let heights = calcRowHeights <$> dh <*> pct
138+ quarters = fst <$> heights
139+ eighths = snd <$> heights
140+ eighthRow = ffor eighths $ \x -> if x == 0 then 0 else 1
141+ grout (fixed eighthRow) $ fill' (current $ eighthBlocks <$> eighths) $ current $
142+ ffor quarters $ \q ->
143+ if | _quarter_fourth q > 0 -> red
144+ | _quarter_third q > 0 -> orange
145+ | _quarter_second q > 0 -> yellow
146+ | otherwise -> white
147+ grout (fixed $ _quarter_fourth <$> quarters) $ fill' (pure '█') (pure red)
148+ grout (fixed $ _quarter_third <$> quarters) $ fill' (pure '█') (pure orange)
149+ grout (fixed $ _quarter_second <$> quarters) $ fill' (pure '█') (pure yellow)
150+ grout (fixed $ _quarter_first <$> quarters) $ fill' (pure '█') (pure white)
151+ where
152+ -- Calculate number of full rows, height of partial row
153+ calcRowHeights :: Int -> Ratio Word64 -> (Quarter Int, Int)
154+ calcRowHeights h r =
155+ let (full, leftovers) = divMod (numerator r * fromIntegral h) (denominator r)
156+ partial = ceiling $ 8 * (leftovers % denominator r)
157+ quarter = ceiling $ fromIntegral h / (4 :: Double)
158+ n = fromIntegral full
159+ in if | n <= quarter ->
160+ (Quarter n 0 0 0, partial)
161+ | n <= (2 * quarter) ->
162+ (Quarter quarter (n - quarter) 0 0, partial)
163+ | n <= (3 * quarter) ->
164+ (Quarter quarter quarter (n - (2 * quarter)) 0, partial)
165+ | otherwise ->
166+ (Quarter quarter quarter quarter (n - (3 * quarter)), partial)
167+ fill' bc attr = do
168+ dw <- displayWidth
169+ dh <- displayHeight
170+ let fillImg =
171+ (\w h c a -> [V.charFill a c w h])
172+ <$> current dw
173+ <*> current dh
174+ <*> bc
175+ <*> attr
176+ tellImages fillImg
177+ color :: Int -> Int -> Int -> V.Color
178+ color = V.rgbColor
179+ red = V.withForeColor V.defAttr $ color 255 0 0
180+ orange = V.withForeColor V.defAttr $ color 255 165 0
181+ yellow = V.withForeColor V.defAttr $ color 255 255 0
182+ white = V.withForeColor V.defAttr $ color 255 255 255
183+
184+data Quarter a = Quarter
185+ { _quarter_first :: a
186+ , _quarter_second :: a
187+ , _quarter_third :: a
188+ , _quarter_fourth :: a
189+ }
190+
191+eighthBlocks :: (Eq a, Num a, Ord a) => a -> Char
192+eighthBlocks n =
193+ if
194+ | n <= 0 -> ' '
195+ | n == 1 -> '▁'
196+ | n == 2 -> '▂'
197+ | n == 3 -> '▃'
198+ | n == 4 -> '▄'
199+ | n == 5 -> '▅'
200+ | n == 6 -> '▆'
201+ | n == 7 -> '▇'
202+ | otherwise -> '█'
203+
204+-- | Determine the current percentage usage according to this algorithm:
205+--
206+-- PrevIdle = previdle + previowait
207+-- Idle = idle + iowait
208+--
209+-- PrevNonIdle = prevuser + prevnice + prevsystem + previrq + prevsoftirq + prevsteal
210+-- NonIdle = user + nice + system + irq + softirq + steal
211+--
212+-- PrevTotal = PrevIdle + PrevNonIdle
213+-- Total = Idle + NonIdle
214+--
215+-- totald = Total - PrevTotal
216+-- idled = Idle - PrevIdle
217+--
218+-- CPU_Percentage = (totald - idled)/totald
219+--
220+-- Source: https://stackoverflow.com/questions/23367857/accurate-calculation-of-cpu-usage-given-in-percentage-in-linux
221+cpuPercentStep
222+ :: (Word64, Word64) -- Current active, Current idle
223+ -> ((Word64, Word64), Ratio Word64) -- (Previous idle, Previous total), previous percent
224+ -> ((Word64, Word64), Ratio Word64) -- (New idle, new total), percent
225+cpuPercentStep (nonidle, idle) ((previdle, prevtotal), _) =
226+ let total = idle + nonidle
227+ idled = idle - previdle
228+ totald = total - prevtotal
229+ in ( (idle, total)
230+ , (totald - idled) % totald
231+ )
src-bin/example.hs+37−21
@@ -1,7 +1,6 @@
11import Control.Applicative
22import Control.Monad
33import Control.Monad.Fix
4−import Control.Monad.NodeId
54import Data.Functor
65import Data.Functor.Misc
76import Data.Map (Map)
@@ -15,49 +14,68 @@ import Reflex
1514import Reflex.Network
1615import Reflex.Vty
1716
17+import Example.CPU
18+
1819type VtyExample t m =
1920 ( MonadFix m
2021 , Reflex t
21− , HasVtyInput t m
22− , HasVtyWidgetCtx t m
23− , ImageWriter t m
24− , MonadNodeId m
25− , HasDisplaySize t m
22+ , HasInput t m
23+ , HasImageWriter t m
24+ , HasDisplayRegion t m
2625 , HasFocus t m
26+ , HasFocusReader t m, HasTheme t m
2727 )
2828
2929type Manager t m =
30− ( MonadLayout t m
31− , MonadFocus t m
30+ ( HasLayout t m
31+ , HasFocus t m
3232 )
3333
3434data Example = Example_TextEditor
3535 | Example_Todo
3636 | Example_ScrollableTextDisplay
3737 | Example_ClickButtonsGetEmojis
38+ | Example_CPUStat
3839 deriving (Show, Read, Eq, Ord, Enum, Bounded)
3940
40−main :: IO ()
41−main = mainWidget $ do
41+withCtrlC :: (Monad m, HasInput t m, Reflex t) => m () -> m (Event t ())
42+withCtrlC f = do
4243 inp <- input
44+ f
45+ return $ fforMaybe inp $ \case
46+ V.EvKey (V.KChar 'c') [V.MCtrl] -> Just ()
47+ _ -> Nothing
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+
57+main :: IO ()
58+main = mainWidget $ withCtrlC $ do
4359 initManager_ $ do
4460 tabNavigation
4561 let gf = grout . fixed
46− tf = tile . fixed
62+ t = tile flex
4763 buttons = col $ do
48− gf 4 $ col $ do
64+ gf 3 $ col $ do
4965 gf 1 $ text "Select an example."
5066 gf 1 $ text "Esc will bring you back here."
5167 gf 1 $ text "Ctrl+c to quit."
52− a <- tf 5 $ textButtonStatic def "Todo List"
53− b <- tf 5 $ textButtonStatic def "Text Editor"
54− c <- tf 5 $ textButtonStatic def "Scrollable text display"
55− d <- tf 5 $ textButtonStatic def "Clickable buttons"
68+ a <- t $ textButtonStatic def "Todo List"
69+ b <- t $ textButtonStatic def "Text Editor"
70+ c <- t $ textButtonStatic def "Scrollable text display"
71+ d <- t $ textButtonStatic def "Clickable buttons"
72+ e <- t $ textButtonStatic def "CPU Usage"
5673 return $ leftmost
5774 [ Left Example_Todo <$ a
5875 , Left Example_TextEditor <$ b
5976 , Left Example_ScrollableTextDisplay <$ c
6077 , Left Example_ClickButtonsGetEmojis <$ d
78+ , Left Example_CPUStat <$ e
6179 ]
6280 let escapable w = do
6381 void w
@@ -67,14 +85,12 @@ main = mainWidget $ do
6785 _ -> Nothing
6886 rec out <- networkHold buttons $ ffor (switch (current out)) $ \case
6987 Left Example_Todo -> escapable taskList
70− Left Example_TextEditor -> escapable testBoxes
88+ Left Example_TextEditor -> escapable $ localTheme (const (constant darkTheme)) testBoxes
7189 Left Example_ScrollableTextDisplay -> escapable scrolling
7290 Left Example_ClickButtonsGetEmojis -> escapable easyExample
91+ Left Example_CPUStat -> escapable cpuStats
7392 Right () -> buttons
7493 return ()
75− return $ fforMaybe inp $ \case
76− V.EvKey (V.KChar 'c') [V.MCtrl] -> Just ()
77− _ -> Nothing
7894
7995-- * Mouse button and emojis example
8096easyExample :: (VtyExample t m, Manager t m, MonadHold t m) => m (Event t ())
@@ -225,7 +241,7 @@ scrolling = col $ do
225241-- * Text editor example with resizable boxes
226242
227243testBoxes
228− :: (MonadHold t m, MonadNodeId m, VtyExample t m)
244+ :: (MonadHold t m, VtyExample t m)
229245 => m ()
230246testBoxes = do
231247 dw <- displayWidth
src/Control/Monad/NodeId.hs+3−1
@@ -10,6 +10,7 @@ module Control.Monad.NodeId
1010 , runNodeIdT
1111 ) where
1212
13+import Control.Monad.Morph
1314import Control.Monad.Reader
1415import Control.Monad.Ref
1516import Data.IORef
@@ -34,10 +35,12 @@ newtype NodeIdT m a = NodeIdT { unNodeIdT :: ReaderT (IORef NodeId) m a }
3435 deriving
3536 ( Functor
3637 , Applicative
38+ , MFunctor
3739 , Monad
3840 , MonadFix
3941 , MonadHold t
4042 , MonadIO
43+ , MonadRef
4144 , MonadReflexCreateTrigger t
4245 , MonadSample t
4346 , MonadTrans
@@ -45,7 +48,6 @@ newtype NodeIdT m a = NodeIdT { unNodeIdT :: ReaderT (IORef NodeId) m a }
4548 , PerformEvent t
4649 , PostBuild t
4750 , TriggerEvent t
48− , MonadRef
4951 )
5052
5153instance MonadNodeId m => MonadNodeId (ReaderT x m)
src/Data/Text/Zipper.hs+280−157
@@ -9,22 +9,26 @@ Description: A zipper for text documents that allows convenient editing and navi
99-}
1010module Data.Text.Zipper where
1111
12+import Prelude
13+
14+import Control.Exception (assert)
15+import Control.Monad.State (evalState, forM, get, put)
1216import Data.Char (isSpace)
1317import Data.Map (Map)
14−import qualified Data.Map as Map
1518import Data.Maybe (fromMaybe)
1619import Data.String
17−import Control.Monad.State (evalState, forM, get, put)
18−
1920import Data.Text (Text)
20−import qualified Data.Text as T
2121import Data.Text.Internal (Text(..), text)
2222import Data.Text.Internal.Fusion (stream)
2323import Data.Text.Internal.Fusion.Types (Stream(..), Step(..))
2424import Data.Text.Unsafe
25+import qualified Data.List as L
26+import qualified Data.Map as Map
27+import qualified Data.Text as T
2528
2629import Graphics.Text.Width (wcwidth)
2730
31+
2832-- | A zipper of the logical text input contents (the "document"). The lines
2933-- before the line containing the cursor are stored in reverse order.
3034-- The cursor is logically between the "before" and "after" text.
@@ -166,7 +170,7 @@ deleteLeftWord (TextZipper lb b a la) =
166170-- | Insert up to n spaces to get to the next logical column that is a multiple of n
167171tab :: Int -> TextZipper -> TextZipper
168172tab n z@(TextZipper _ b _ _) =
169− insert (T.replicate (fromEnum $ n - (T.length b `mod` max 1 n)) " ") z
173+ insert (T.replicate (fromEnum $ n - T.length b `mod` max 1 n) " ") z
170174
171175-- | The plain text contents of the zipper
172176value :: TextZipper -> Text
@@ -189,105 +193,35 @@ fromText = flip insert empty
189193data Span tag = Span tag Text
190194 deriving (Show)
191195
196+-- | Text alignment type
197+data TextAlignment =
198+ TextAlignment_Left
199+ | TextAlignment_Right
200+ | TextAlignment_Center
201+ deriving (Eq, Show)
202+
203+-- A map from the index (row) of display line to (fst,snd)
204+-- fst: leading empty spaces from left (may be negative) to adjust for alignment
205+-- snd: the text offset from the beginning of the document
206+-- to the first character of the display line
207+type OffsetMapWithAlignment = Map Int (Int, Int)
208+
209+-- helper type representing a single visual line that may be part of a wrapped logical line
210+data WrappedLine = WrappedLine
211+ { _wrappedLines_text :: Text
212+ , _wrappedLines_hiddenWhitespace :: Bool -- ^ 'True' if this line ends with a deleted whitespace character
213+ , _wrappedLines_offset :: Int -- ^ offset from beginning of line
214+ }
215+ deriving (Eq, Show)
216+
192217-- | Information about the document as it is displayed (i.e., post-wrapping)
193218data DisplayLines tag = DisplayLines
194219 { _displayLines_spans :: [[Span tag]]
195− , _displayLines_offsetMap :: Map Int Int
196− , _displayLines_cursorY :: Int
220+ , _displayLines_offsetMap :: OffsetMapWithAlignment
221+ , _displayLines_cursorPos :: (Int, Int) -- cursor position relative to upper left hand corner
197222 }
198223 deriving (Show)
199224
200−-- | Given a width and a 'TextZipper', produce a list of display lines
201−-- (i.e., lines of wrapped text) with special attributes applied to
202−-- certain segments (e.g., the cursor). Additionally, produce the current
203−-- y-coordinate of the cursor and a mapping from display line number to text
204−-- offset
205−displayLines
206− :: Int -- ^ Width, used for wrapping
207− -> tag -- ^ Metadata for normal characters
208− -> tag -- ^ Metadata for the cursor
209− -> TextZipper -- ^ The text input contents and cursor state
210− -> DisplayLines tag
211−displayLines width tag cursorTag (TextZipper lb b a la) =
212− let linesBefore :: [[Text]] -- The wrapped lines before the cursor line
213− linesBefore = map (wrapWithOffset width 0) $ reverse lb
214− linesAfter :: [[Text]] -- The wrapped lines after the cursor line
215− linesAfter = map (wrapWithOffset width 0) la
216− offsets :: Map Int Int
217− offsets = offsetMap $ mconcat
218− [ linesBefore
219− , [wrapWithOffset width 0 $ b <> a]
220− , linesAfter
221− ]
222− spansBefore = map ((:[]) . Span tag) $ concat linesBefore
223− spansAfter = map ((:[]) . Span tag) $ concat linesAfter
224− -- Separate the spans before the cursor into
225− -- * spans that are on earlier display lines (though on the same logical line), and
226− -- * spans that are on the same display line
227− (spansCurrentBefore, spansCurLineBefore) = fromMaybe ([], []) $
228− initLast $ map ((:[]) . Span tag) (wrapWithOffset width 0 b)
229− -- Calculate the number of columns on the cursor's display line before the cursor
230− curLineOffset = spansWidth spansCurLineBefore
231− -- Check whether the spans on the current display line are long enough that
232− -- the cursor has to go to the next line
233− cursorAfterEOL = curLineOffset == width
234− cursorCharWidth = case T.uncons a of
235− Nothing -> 1
236− Just (c, _) -> charWidth c
237− -- Separate the span after the cursor into
238− -- * spans that are on the same display line, and
239− -- * spans that are on later display lines (though on the same logical line)
240− (spansCurLineAfter, spansCurrentAfter) = fromMaybe ([], []) $
241− headTail $ case T.uncons a of
242− Nothing -> [[Span cursorTag " "]]
243− Just (c, rest) ->
244− let o = if cursorAfterEOL then cursorCharWidth else curLineOffset + cursorCharWidth
245− cursor = Span cursorTag (T.singleton c)
246− in case map ((:[]) . Span tag) (wrapWithOffset width o rest) of
247− [] -> [[cursor]]
248− (l:ls) -> (cursor : l) : ls
249− in DisplayLines
250− { _displayLines_spans = concat
251− [ spansBefore
252− , spansCurrentBefore
253− , if cursorAfterEOL
254− then [ spansCurLineBefore, spansCurLineAfter ]
255− else [ spansCurLineBefore <> spansCurLineAfter ]
256− , spansCurrentAfter
257− , spansAfter
258− ]
259− , _displayLines_offsetMap = offsets
260− , _displayLines_cursorY = sum
261− [ length spansBefore
262− , length spansCurrentBefore
263− , if cursorAfterEOL then 1 else 0
264− ]
265− }
266− where
267− initLast :: [a] -> Maybe ([a], a)
268− initLast = \case
269− [] -> Nothing
270− (x:xs) -> case initLast xs of
271− Nothing -> Just ([], x)
272− Just (ys, y) -> Just (x:ys, y)
273− headTail :: [a] -> Maybe (a, [a])
274− headTail = \case
275− [] -> Nothing
276− x:xs -> Just (x, xs)
277−
278−-- | Wraps a logical line of text to fit within the given width. The first
279−-- wrapped line is offset by the number of columns provided. Subsequent wrapped
280−-- lines are not.
281−wrapWithOffset
282− :: Int -- ^ Maximum width
283− -> Int -- ^ Offset for first line
284− -> Text -- ^ Text to be wrapped
285− -> [Text]
286−wrapWithOffset maxWidth _ _ | maxWidth <= 0 = []
287−wrapWithOffset maxWidth n xs =
288− let (firstLine, rest) = splitAtWidth (maxWidth - n) xs
289− in firstLine : (fmap (takeWidth maxWidth) . takeWhile (not . T.null) . iterate (dropWidth maxWidth) $ rest)
290−
291225-- | Split a 'Text' at the given column index. For example
292226--
293227-- > splitAtWidth 3 "ᄀabc" == ("ᄀa", "bc")
@@ -297,16 +231,16 @@ splitAtWidth :: Int -> Text -> (Text, Text)
297231splitAtWidth n t@(Text arr off len)
298232 | n <= 0 = (T.empty, t)
299233 | n >= textWidth t = (t, T.empty)
300− | otherwise = let k = iterNWidth n t
234+ | otherwise = let k = toLogicalIndex n t
301235 in (text arr off k, text arr (off+k) (len-k))
302− where
303− iterNWidth :: Int -> Text -> Int
304− iterNWidth n' t'@(Text _ _ len') = loop 0 0
305− where loop !i !cnt
306− | i >= len' || cnt + w > n' = i
307− | otherwise = loop (i+d) (cnt + w)
308− where Iter c d = iter t' i
309− w = charWidth c
236+
237+toLogicalIndex :: Int -> Text -> Int
238+toLogicalIndex n' t'@(Text _ _ len') = loop 0 0
239+ where loop !i !cnt
240+ | i >= len' || cnt + w > n' = i
241+ | otherwise = loop (i+d) (cnt + w)
242+ where Iter c d = iter t' i
243+ w = charWidth c
310244
311245-- | Takes the given number of columns of characters. For example
312246--
@@ -335,56 +269,6 @@ dropWidth n = snd . splitAtWidth n
335269charWidth :: Char -> Int
336270charWidth = wcwidth
337271
338−
339−-- | For a given set of wrapped logical lines, computes a map
340−-- from display line index to text offset in the original text.
341−-- This is used to help determine how interactions with the displayed
342−-- text map back to the original text.
343−-- For example, given the document @\"AA\\nBBB\\nCCCCCCCC\\n\"@ wrapped to 5 columns,
344−-- this function will compute the offset in the original document of each character
345−-- in column 1:
346−--
347−-- > AA... (0, 0)
348−-- > BBB.. (1, 3)
349−-- > CCCCC (2, 7) -- (this line wraps to the next row)
350−-- > CCC.. (3, 12)
351−-- > ..... (4, 16)
352−offsetMap
353− :: [[Text]] -- ^ The outer list represents logical lines, and the
354− -- inner list represents the display lines into which
355− -- the logical line has been wrapped
356− -> Map Int Int -- ^ A map from the index (row) of display line to
357− -- the text offset from the beginning of the document
358− -- to the first character of the display line
359−offsetMap ts = evalState (offsetMap' ts) (0, 0)
360− where
361− offsetMap' xs = fmap Map.unions $ forM xs $ \x -> do
362− maps <- forM x $ \line -> do
363− let l = T.length line
364− (dl, o) <- get
365− put (dl + 1, o + l)
366− return $ Map.singleton dl o
367− (dl, o) <- get
368− put (dl, o + 1)
369− return $ Map.insert dl (o + 1) $ Map.unions maps
370−
371−-- | Move the cursor of the given 'TextZipper' to the logical position indicated
372−-- by the given display line coordinates, using the provided 'DisplayLines'
373−-- information. If the x coordinate is beyond the end of a line, the cursor is
374−-- moved to the end of the line.
375−goToDisplayLinePosition :: Int -> Int -> DisplayLines tag -> TextZipper -> TextZipper
376−goToDisplayLinePosition x y dl tz =
377− let offset = Map.lookup y $ _displayLines_offsetMap dl
378− in case offset of
379− Nothing -> tz
380− Just o ->
381− let
382− moveRight = case drop y $ _displayLines_spans dl of
383− [] -> x
384− (s:_) -> charIndexAt x . stream . mconcat . fmap (\(Span _ t) -> t) $ s
385− in rightN (o + min moveRight x) $ top tz
386−
387−
388272-- | Get the width of the text in a set of 'Span's, taking into account unicode character widths
389273spansWidth :: [Span tag] -> Int
390274spansWidth = sum . map (\(Span _ t) -> textWidth t)
@@ -420,3 +304,242 @@ charIndexAt pos (Stream next s0 _len) = loop_length 0 0 s0
420304 Yield c s' -> if w > pos then i else loop_length (i+1) w s' where
421305 w = z + charWidth c
422306{-# INLINE[0] charIndexAt #-}
307+
308+
309+
310+
311+-- | Same as T.words except whitespace characters are included at end (i.e. ["line1 ", ...])
312+-- 'Char's representing white space.
313+wordsWithWhitespace :: Text -> [Text]
314+wordsWithWhitespace t@(Text arr off len) = loop 0 0 False
315+ where
316+ loop !start !n !wasSpace
317+ | n >= len = [Text arr (start+off) (n-start) | not (start == n)]
318+ | isSpace c = loop start (n+d) True
319+ | wasSpace = Text arr (start+off) (n-start) : loop n n False
320+ | otherwise = loop start (n+d) False
321+ where Iter c d = iter t n
322+{-# INLINE wordsWithWhitespace #-}
323+
324+-- | Split words into logical lines, 'True' in the tuple indicates line ends with a whitespace character that got deleted
325+splitWordsAtDisplayWidth :: Int -> [Text] -> [(Text, Bool)]
326+splitWordsAtDisplayWidth maxWidth wwws = reverse $ loop wwws 0 [] where
327+ appendOut :: [(Text,Bool)] -> Text -> Bool -> [(Text,Bool)]
328+ appendOut [] t b = [(t,b)]
329+ appendOut ((t',_):ts') t b = (t'<>t,b) : ts'
330+
331+ -- remove the last whitespace in output
332+ modifyOutForNewLine :: [(Text,Bool)] -> [(Text,Bool)]
333+ modifyOutForNewLine [] = error "should never happen"
334+ modifyOutForNewLine ((t',_):ts) = case T.unsnoc t' of
335+ Nothing -> error "should never happen"
336+ Just (t,lastChar) -> assert (isSpace lastChar) $ (t,True):ts -- assume last char is whitespace
337+
338+ loop :: [Text] -> Int -> [(Text,Bool)] -> [(Text,Bool)]
339+ loop [] _ out = out
340+ loop (x:xs) cumw out = r where
341+ newWidth = textWidth x + cumw
342+ r = if newWidth > maxWidth
343+ then if isSpace $ T.index x (toLogicalIndex (maxWidth - cumw) x)
344+ -- if line runs over but character of splitting is whitespace then split on the whitespace
345+ then let (t1,t2) = splitAtWidth (maxWidth - cumw) x
346+ in loop (T.drop 1 t2:xs) 0 [] <> appendOut out t1 True
347+ else if cumw == 0
348+ -- single word exceeds max width, so just split on the word
349+ then let (t1,t2) = splitAtWidth (maxWidth - cumw) x
350+ in loop (t2:xs) 0 [] <> appendOut out t1 False
351+ -- otherwise start a new line
352+ else loop (x:xs) 0 [] <> modifyOutForNewLine out
353+ else loop xs newWidth $ appendOut out x False
354+
355+
356+
357+-- | Wraps a logical line of text to fit within the given width. The first
358+-- wrapped line is offset by the number of columns provided. Subsequent wrapped
359+-- lines are not.
360+wrapWithOffsetAndAlignment
361+ :: TextAlignment
362+ -> Int -- ^ Maximum width
363+ -> Int -- ^ Offset for first line
364+ -> Text -- ^ Text to be wrapped
365+ -> [WrappedLine] -- (words on that line, hidden space char, offset from beginning of line)
366+wrapWithOffsetAndAlignment _ maxWidth _ _ | maxWidth <= 0 = []
367+wrapWithOffsetAndAlignment alignment maxWidth n txt = assert (n <= maxWidth) r where
368+ -- we pad by offset amount with any non-space character which we will remove later so that no changes need to be made to splitWordsAtDisplayWidth
369+ r' = splitWordsAtDisplayWidth maxWidth $ wordsWithWhitespace ( T.replicate n "." <> txt)
370+ fmapfn (t,b) = case alignment of
371+ TextAlignment_Left -> WrappedLine t b 0
372+ TextAlignment_Right -> WrappedLine t b (maxWidth-l)
373+ TextAlignment_Center -> WrappedLine t b ((maxWidth-l) `div` 2)
374+ where l = textWidth t
375+ r'' = case r' of
376+ [] -> []
377+ (x,b):xs -> (T.drop n x,b):xs
378+ r = fmap fmapfn r''
379+
380+-- converts deleted eol spaces into logical lines
381+eolSpacesToLogicalLines :: [[WrappedLine]] -> [[(Text, Int)]]
382+eolSpacesToLogicalLines = fmap (fmap (\(WrappedLine a _ c) -> (a,c))) . concatMap (L.groupBy (\(WrappedLine _ b _) _ -> not b))
383+
384+offsetMapWithAlignmentInternal :: [[WrappedLine]] -> OffsetMapWithAlignment
385+offsetMapWithAlignmentInternal = offsetMapWithAlignment . eolSpacesToLogicalLines
386+
387+offsetMapWithAlignment
388+ :: [[(Text, Int)]] -- ^ The outer list represents logical lines, inner list represents wrapped lines
389+ -> OffsetMapWithAlignment
390+offsetMapWithAlignment ts = evalState (offsetMap' ts) (0, 0)
391+ where
392+ offsetMap' xs = fmap Map.unions $ forM xs $ \x -> do
393+ maps <- forM x $ \(line,align) -> do
394+ let l = T.length line
395+ (dl, o) <- get
396+ put (dl + 1, o + l)
397+ return $ Map.singleton dl (align, o)
398+ (dl, o) <- get
399+ put (dl, o + 1)
400+ -- add additional offset to last line in wrapped lines (for newline char)
401+ return $ Map.adjust (\(align,_)->(align,o+1)) dl $ Map.unions maps
402+
403+
404+-- | Given a width and a 'TextZipper', produce a list of display lines
405+-- (i.e., lines of wrapped text) with special attributes applied to
406+-- certain segments (e.g., the cursor). Additionally, produce the current
407+-- y-coordinate of the cursor and a mapping from display line number to text
408+-- offset
409+displayLinesWithAlignment
410+ :: TextAlignment
411+ -> Int -- ^ Width, used for wrapping
412+ -> tag -- ^ Metadata for normal characters
413+ -> tag -- ^ Metadata for the cursor
414+ -> TextZipper -- ^ The text input contents and cursor state
415+ -> DisplayLines tag
416+displayLinesWithAlignment alignment width tag cursorTag (TextZipper lb b a la) =
417+ let linesBefore :: [[WrappedLine]] -- The wrapped lines before the cursor line
418+ linesBefore = map (wrapWithOffsetAndAlignment alignment width 0) $ reverse lb
419+ linesAfter :: [[WrappedLine]] -- The wrapped lines after the cursor line
420+ linesAfter = map (wrapWithOffsetAndAlignment alignment width 0) la
421+
422+ -- simulate trailing cursor character when computing OffsetMap
423+ afterWithCursor = if T.null a then " " else a
424+ offsets :: OffsetMapWithAlignment
425+ offsets = offsetMapWithAlignmentInternal $ mconcat
426+ [ linesBefore
427+ , [wrapWithOffsetAndAlignment alignment width 0 $ b <> afterWithCursor]
428+ , linesAfter
429+ ]
430+ flattenLines = concatMap (fmap _wrappedLines_text)
431+ spansBefore = map ((:[]) . Span tag) $ flattenLines linesBefore
432+ spansAfter = map ((:[]) . Span tag) $ flattenLines linesAfter
433+ -- Separate the spans before the cursor into
434+ -- * spans that are on earlier display lines (though on the same logical line), and
435+ -- * spans that are on the same display line
436+
437+ (spansCurrentBefore, spansCurLineBefore) = fromMaybe ([], []) $
438+ initLast $ map ((:[]) . Span tag) $ _wrappedLines_text <$> (wrapWithOffsetAndAlignment alignment width 0 b)
439+ -- Calculate the number of columns on the cursor's display line before the cursor
440+ curLineOffset = spansWidth spansCurLineBefore
441+ -- Check whether the spans on the current display line are long enough that
442+ -- the cursor has to go to the next line
443+ cursorAfterEOL = curLineOffset == width
444+ cursorCharWidth = case T.uncons a of
445+ Nothing -> 1
446+ Just (c, _) -> charWidth c
447+
448+ -- Separate the span after the cursor into
449+ -- * spans that are on the same display line, and
450+ -- * spans that are on later display lines (though on the same logical line)
451+
452+ (spansCurLineAfter, spansCurrentAfter) = fromMaybe ([], []) $
453+ headTail $ case T.uncons a of
454+ Nothing -> [[Span cursorTag " "]]
455+ Just (c, rest) ->
456+ let o = if cursorAfterEOL then cursorCharWidth else curLineOffset + cursorCharWidth
457+ cursor = Span cursorTag (T.singleton c)
458+ in case map ((:[]) . Span tag) $ _wrappedLines_text <$> (wrapWithOffsetAndAlignment alignment width o rest) of
459+ [] -> [[cursor]]
460+ (l:ls) -> (cursor : l) : ls
461+
462+ curLineSpanNormalCase = if cursorAfterEOL
463+ then [ spansCurLineBefore, spansCurLineAfter ]
464+ else [ spansCurLineBefore <> spansCurLineAfter ]
465+
466+ -- for right alignment, we want draw the cursor tag to be on the character just before the logical cursor position
467+ curLineSpan = if alignment == TextAlignment_Right && not cursorAfterEOL
468+ then case reverse spansCurLineBefore of
469+ [] -> curLineSpanNormalCase
470+ (Span _ x):xs -> case spansCurLineAfter of
471+ [] -> error "should not be possible" -- curLineSpanNormalCase
472+ (Span _ y):ys -> [reverse (Span cursorTag x:xs) <> ((Span tag y):ys)]
473+ else curLineSpanNormalCase
474+
475+ cursorY = sum
476+ [ length spansBefore
477+ , length spansCurrentBefore
478+ , if cursorAfterEOL then 1 else 0
479+ ]
480+ -- a little silly to convert back to text but whatever, it works
481+ cursorX = if cursorAfterEOL then 0 else textWidth (mconcat $ fmap (\(Span _ t) -> t) spansCurLineBefore)
482+
483+ in DisplayLines
484+ { _displayLines_spans = concat
485+ [ spansBefore
486+ , spansCurrentBefore
487+ , curLineSpan
488+ , spansCurrentAfter
489+ , spansAfter
490+ ]
491+ , _displayLines_offsetMap = offsets
492+ , _displayLines_cursorPos = (cursorX, cursorY)
493+ }
494+ where
495+ initLast :: [a] -> Maybe ([a], a)
496+ initLast = \case
497+ [] -> Nothing
498+ (x:xs) -> case initLast xs of
499+ Nothing -> Just ([], x)
500+ Just (ys, y) -> Just (x:ys, y)
501+ headTail :: [a] -> Maybe (a, [a])
502+ headTail = \case
503+ [] -> Nothing
504+ x:xs -> Just (x, xs)
505+
506+
507+-- | Move the cursor of the given 'TextZipper' to the logical position indicated
508+-- by the given display line coordinates, using the provided 'DisplayLinesWithAlignment'
509+-- information. If the x coordinate is beyond the end of a line, the cursor is
510+-- moved to the end of the line.
511+goToDisplayLinePosition :: Int -> Int -> DisplayLines tag -> TextZipper -> TextZipper
512+goToDisplayLinePosition x y dl tz =
513+ let offset = Map.lookup y $ _displayLines_offsetMap dl
514+ in case offset of
515+ Nothing -> tz
516+ Just (alignOff,o) ->
517+ let
518+ trueX = max 0 (x - alignOff)
519+ moveRight = case drop y $ _displayLines_spans dl of
520+ [] -> 0
521+ (s:_) -> charIndexAt trueX . stream . mconcat . fmap (\(Span _ t) -> t) $ s
522+ in rightN (o + moveRight) $ top tz
523+
524+-- | Given a width and a 'TextZipper', produce a list of display lines
525+-- (i.e., lines of wrapped text) with special attributes applied to
526+-- certain segments (e.g., the cursor). Additionally, produce the current
527+-- y-coordinate of the cursor and a mapping from display line number to text
528+-- offset
529+displayLines
530+ :: Int -- ^ Width, used for wrapping
531+ -> tag -- ^ Metadata for normal characters
532+ -> tag -- ^ Metadata for the cursor
533+ -> TextZipper -- ^ The text input contents and cursor state
534+ -> DisplayLines tag
535+displayLines = displayLinesWithAlignment TextAlignment_Left
536+
537+-- | Wraps a logical line of text to fit within the given width. The first
538+-- wrapped line is offset by the number of columns provided. Subsequent wrapped
539+-- lines are not.
540+wrapWithOffset
541+ :: Int -- ^ Maximum width
542+ -> Int -- ^ Offset for first line
543+ -> Text -- ^ Text to be wrapped
544+ -> [Text]
545+wrapWithOffset maxWidth n xs = _wrappedLines_text <$> wrapWithOffsetAndAlignment TextAlignment_Left maxWidth n xs
src/Reflex/Class/Switchable.hsdeleted−27
@@ -1,27 +0,0 @@
1−{-|
2−Module: Reflex.Class.Switchable
3−Description: A class for things that can be switched on the firing of an event
4−-}
5−{-# Language UndecidableInstances #-}
6−module Reflex.Class.Switchable where
7−
8−import Control.Monad
9−import Reflex
10−
11−-- | Class representing things that can be switched when the provided event occurs
12−class Reflex t => Switchable t w | w -> t where
13− switching :: MonadHold t m => w -> Event t w -> m w
14−
15−instance Reflex t => Switchable t (Event t a) where
16− switching = switchHold
17−
18−instance Reflex t => Switchable t (Dynamic t a) where
19− switching a e = fmap join $ holdDyn a e
20−
21−instance Reflex t => Switchable t (Behavior t a) where
22− switching = switcher
23−
24−instance (Reflex t, Switchable t a, Switchable t b) => Switchable t (a, b) where
25− switching (a, b) e = (,)
26− <$> switching a (fmap fst e)
27− <*> switching b (fmap snd e)
src/Reflex/Vty.hs+6
@@ -14,15 +14,21 @@ module Reflex.Vty
1414 ( module Reflex
1515 , module Reflex.Vty.Host
1616 , module Reflex.Vty.Widget
17+ , module Reflex.Vty.Widget.Box
1718 , module Reflex.Vty.Widget.Input
1819 , module Reflex.Vty.Widget.Layout
20+ , module Reflex.Vty.Widget.Split
21+ , module Reflex.Vty.Widget.Text
1922 , module Control.Monad.NodeId
2023 ) where
2124
2225import Reflex
2326import Reflex.Vty.Host
2427import Reflex.Vty.Widget
28+import Reflex.Vty.Widget.Box
2529import Reflex.Vty.Widget.Input
2630import Reflex.Vty.Widget.Layout
31+import Reflex.Vty.Widget.Split
32+import Reflex.Vty.Widget.Text
2733
2834import Control.Monad.NodeId
src/Reflex/Vty/Widget.hs+457−616
@@ -4,71 +4,18 @@ Description: Basic set of widgets and building blocks for reflex-vty application
44-}
55{-# Language UndecidableInstances #-}
66
7−module Reflex.Vty.Widget
8− ( VtyWidgetCtx(..)
9− , VtyWidget(..)
10− , VtyWidgetOut(..)
11− , ImageWriter(..)
12− , runVtyWidget
13− , mainWidget
14− , mainWidgetWithHandle
15− , HasDisplaySize(..)
16− , HasFocus(..)
17− , HasVtyInput(..)
18− , HasVtyWidgetCtx(..)
19− , Region(..)
20− , regionSize
21− , regionBlankImage
22− , Drag(..)
23− , drag
24− , MouseDown(..)
25− , MouseUp(..)
26− , mouseDown
27− , mouseUp
28− , ScrollDirection(..)
29− , mouseScroll
30− , pane
31− , splitV
32− , splitH
33− , splitVDrag
34− , boxTitle
35− , box
36− , boxStatic
37− , RichTextConfig(..)
38− , richText
39− , text
40− , scrollableText
41− , display
42− , BoxStyle(..)
43− , hyphenBoxStyle
44− , singleBoxStyle
45− , roundedBoxStyle
46− , thickBoxStyle
47− , doubleBoxStyle
48− , fill
49− , hRule
50− , filterKeys
51− , KeyCombo
52− , key
53− , keys
54− , keyCombo
55− , keyCombos
56− , blank
57− ) where
7+module Reflex.Vty.Widget where
588
599import Control.Applicative (liftA2)
6010import Control.Monad.Fix (MonadFix)
6111import Control.Monad.IO.Class (MonadIO)
6212import Control.Monad.Morph
6313import Control.Monad.NodeId
64−import Control.Monad.Reader (ReaderT, ask, asks, local, runReaderT)
14+import Control.Monad.Reader (ReaderT, ask, local, runReaderT)
15+import Control.Monad.Ref
6516import Control.Monad.Trans (MonadTrans, lift)
66−import Data.Default (Default(..))
6717import Data.Set (Set)
6818import qualified Data.Set as Set
69−import Data.Text (Text)
70−import qualified Data.Text as T
71−import qualified Data.Text.Zipper as TZ
7219import Graphics.Vty (Image)
7320import qualified Graphics.Vty as V
7421import Reflex
@@ -76,86 +23,20 @@ import Reflex.Class ()
7623import Reflex.Host.Class (MonadReflexCreateTrigger)
7724import Reflex.Vty.Host
7825
79−-- | The context within which a 'VtyWidget' runs
80−data VtyWidgetCtx t = VtyWidgetCtx
81− { _vtyWidgetCtx_width :: Dynamic t Int
82− -- ^ The width of the region allocated to the widget.
83− , _vtyWidgetCtx_height :: Dynamic t Int
84− -- ^ The height of the region allocated to the widget.
85− , _vtyWidgetCtx_focus :: Dynamic t Bool
86− -- ^ Whether the widget should behave as if it has focus for keyboard input.
87− }
88−
89−-- | The output of a 'VtyWidget'
90−data VtyWidgetOut t = VtyWidgetOut
91− { _vtyWidgetOut_shutdown :: Event t ()
92− }
93−
94−instance (Adjustable t m, MonadHold t m, Reflex t) => Adjustable t (VtyWidget t m) where
95− runWithReplace a0 a' = VtyWidget $ runWithReplace (unVtyWidget a0) $ fmap unVtyWidget a'
96− traverseIntMapWithKeyWithAdjust f dm0 dm' = VtyWidget $
97− traverseIntMapWithKeyWithAdjust (\k v -> unVtyWidget (f k v)) dm0 dm'
98− traverseDMapWithKeyWithAdjust f dm0 dm' = VtyWidget $ do
99− traverseDMapWithKeyWithAdjust (\k v -> unVtyWidget (f k v)) dm0 dm'
100− traverseDMapWithKeyWithAdjustWithMove f dm0 dm' = VtyWidget $ do
101− traverseDMapWithKeyWithAdjustWithMove (\k v -> unVtyWidget (f k v)) dm0 dm'
102−
103−-- | A widget that can read its context and produce image output
104−newtype VtyWidget t m a = VtyWidget
105− { unVtyWidget :: BehaviorWriterT t [Image]
106− (ReaderT (VtyWidgetCtx t)
107− (ReaderT (Event t VtyEvent) m)) a
108− } deriving
109− ( Functor
110− , Applicative
111− , Monad
112− , MonadSample t
113− , MonadHold t
114− , MonadFix
115− , NotReady t
116− , ImageWriter t
117− , PostBuild t
118− , TriggerEvent t
119− , MonadReflexCreateTrigger t
120− , MonadIO
121− )
122−
123−deriving instance PerformEvent t m => PerformEvent t (VtyWidget t m)
124−
125−instance MonadTrans (VtyWidget t) where
126− lift f = VtyWidget $ lift $ lift $ lift f
127−
128−instance MFunctor (VtyWidget t) where
129− hoist f = VtyWidget . hoist (hoist (hoist f)) . unVtyWidget
130−
131−instance MonadNodeId m => MonadNodeId (VtyWidget t m) where
132− getNextNodeId = VtyWidget $ do
133− lift $ lift getNextNodeId
26+-- * Running a vty application
13427
135−-- | A reader-like class for the vty widget context. Allows actions to be run in a sub-context.
136−class HasVtyWidgetCtx t m | m -> t where
137− askCtx :: m (VtyWidgetCtx t)
138− default askCtx :: (f m' ~ m, Monad m', MonadTrans f, HasVtyWidgetCtx t m') => m (VtyWidgetCtx t)
139− askCtx = lift askCtx
140− localCtx :: (VtyWidgetCtx t -> VtyWidgetCtx t) -> m a -> m a
141−
142−instance (Monad m, Reflex t) => HasVtyWidgetCtx t (VtyWidget t m) where
143− askCtx = VtyWidget $ lift ask
144− localCtx f (VtyWidget x) = VtyWidget $ local f x
145−
146−-- | Runs a 'VtyWidget' with a given context
147−runVtyWidget
148− :: (Reflex t, MonadNodeId m)
149− => Event t VtyEvent
150− -> VtyWidgetCtx t
151− -> VtyWidget t m a
152− -> m (a, Behavior t [Image])
153−runVtyWidget e ctx w = runReaderT (runReaderT (runBehaviorWriterT (unVtyWidget w)) ctx) e
154−
155−-- | Sets up the top-level context for a 'VtyWidget' and runs it with that context
28+-- | Sets up the top-level context for a vty widget and runs it with that context
15629mainWidgetWithHandle
15730 :: V.Vty
158− -> (forall t m. (MonadVtyApp t m, MonadNodeId m) => VtyWidget t m (Event t ()))
31+ -> (forall t m.
32+ ( MonadVtyApp t m
33+ , HasImageWriter t m
34+ , MonadNodeId m
35+ , HasDisplayRegion t m
36+ , HasFocusReader t m
37+ , HasInput t m
38+ , HasTheme t m
39+ ) => m (Event t ()))
15940 -> IO ()
16041mainWidgetWithHandle vty child =
16142 runVtyAppWithHandle vty $ \dr0 inp -> do
@@ -165,109 +46,475 @@ mainWidgetWithHandle vty child =
16546 let inp' = fforMaybe inp $ \case
16647 V.EvResize {} -> Nothing
16748 x -> Just x
168− let ctx = VtyWidgetCtx
169− { _vtyWidgetCtx_width = fmap fst size
170− , _vtyWidgetCtx_height = fmap snd size
171− , _vtyWidgetCtx_focus = constDyn True
172− }
173− (shutdown, images) <- runNodeIdT $ runVtyWidget inp' ctx $ do
174− tellImages . ffor (current size) $ \(w, h) -> [V.charFill V.defAttr ' ' w h]
175− child
49+ (shutdown, images) <- runThemeReader (constant V.defAttr) $
50+ runFocusReader (pure True) $
51+ runDisplayRegion (fmap (\(w, h) -> Region 0 0 w h) size) $
52+ runImageWriter $
53+ runNodeIdT $
54+ runInput inp' $ do
55+ tellImages . ffor (current size) $ \(w, h) -> [V.charFill V.defAttr ' ' w h]
56+ child
17657 return $ VtyResult
17758 { _vtyResult_picture = fmap (V.picForLayers . reverse) images
17859 , _vtyResult_shutdown = shutdown
17960 }
18061
62+-- | The output of a vty widget
63+data VtyWidgetOut t = VtyWidgetOut
64+ { _vtyWidgetOut_shutdown :: Event t ()
65+ }
66+
18167-- | Like 'mainWidgetWithHandle', but uses a default vty configuration
18268mainWidget
183− :: (forall t m. (MonadVtyApp t m, MonadNodeId m) => VtyWidget t m (Event t ()))
69+ :: (forall t m.
70+ ( MonadVtyApp t m
71+ , HasImageWriter t m
72+ , MonadNodeId m
73+ , HasDisplayRegion t m
74+ , HasFocusReader t m
75+ , HasTheme t m
76+ , HasInput t m
77+ ) => m (Event t ()))
18478 -> IO ()
18579mainWidget child = do
18680 vty <- getDefaultVty
18781 mainWidgetWithHandle vty child
18882
189−-- | A class for things that know their own display size dimensions
190−class (Reflex t, Monad m) => HasDisplaySize t m | m -> t where
191− -- | Retrieve the display width (columns)
192− displayWidth :: m (Dynamic t Int)
193− default displayWidth :: (f m' ~ m, MonadTrans f, HasDisplaySize t m') => m (Dynamic t Int)
194− displayWidth = lift displayWidth
195− -- | Retrieve the display height (rows)
196− displayHeight :: m (Dynamic t Int)
197− default displayHeight :: (f m' ~ m, MonadTrans f, HasDisplaySize t m') => m (Dynamic t Int)
198− displayHeight = lift displayHeight
199−
200−instance (Reflex t, Monad m) => HasDisplaySize t (VtyWidget t m) where
201− displayWidth = VtyWidget . lift $ asks _vtyWidgetCtx_width
202− displayHeight = VtyWidget . lift $ asks _vtyWidgetCtx_height
203−
204−instance HasDisplaySize t m => HasDisplaySize t (ReaderT x m)
205−instance HasDisplaySize t m => HasDisplaySize t (BehaviorWriterT t x m)
206−instance HasDisplaySize t m => HasDisplaySize t (DynamicWriterT t x m)
207−instance HasDisplaySize t m => HasDisplaySize t (EventWriterT t x m)
208−instance HasDisplaySize t m => HasDisplaySize t (NodeIdT m)
83+-- * Input Events
20984
21085-- | A class for things that can receive vty events as input
211−class HasVtyInput t m | m -> t where
86+class HasInput t m | m -> t where
21287 input :: m (Event t VtyEvent)
213− default input :: (f m' ~ m, Monad m', MonadTrans f, HasVtyInput t m') => m (Event t VtyEvent)
88+ default input :: (f m' ~ m, Monad m', MonadTrans f, HasInput t m') => m (Event t VtyEvent)
21489 input = lift input
21590 -- | User input events that the widget's parent chooses to share. These will generally
21691 -- be filtered for relevance.
21792 localInput :: (Event t VtyEvent -> Event t VtyEvent) -> m a -> m a
93+ default localInput :: (f m' ~ m, Monad m', MFunctor f, HasInput t m') => (Event t VtyEvent -> Event t VtyEvent) -> m a -> m a
94+ localInput f = hoist (localInput f)
95+
96+instance (Reflex t, Monad m) => HasInput t (Input t m) where
97+ input = Input ask
98+ localInput f (Input m) = Input $ local f m
99+
100+-- | A widget that can receive input events. See 'Graphics.Vty.Event'
101+newtype Input t m a = Input
102+ { unInput :: ReaderT (Event t VtyEvent) m a
103+ } deriving
104+ ( Functor
105+ , Applicative
106+ , Monad
107+ , MonadSample t
108+ , MonadHold t
109+ , MonadFix
110+ , MonadIO
111+ , MonadRef
112+ )
113+
114+instance (Adjustable t m, MonadHold t m, Reflex t) => Adjustable t (Input t m) where
115+ runWithReplace a0 a' = Input $ runWithReplace (unInput a0) $ fmap unInput a'
116+ traverseIntMapWithKeyWithAdjust f dm0 dm' = Input $
117+ traverseIntMapWithKeyWithAdjust (\k v -> unInput (f k v)) dm0 dm'
118+ traverseDMapWithKeyWithAdjust f dm0 dm' = Input $ do
119+ traverseDMapWithKeyWithAdjust (\k v -> unInput (f k v)) dm0 dm'
120+ traverseDMapWithKeyWithAdjustWithMove f dm0 dm' = Input $ do
121+ traverseDMapWithKeyWithAdjustWithMove (\k v -> unInput (f k v)) dm0 dm'
122+
123+deriving instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (Input t m)
124+deriving instance NotReady t m => NotReady t (Input t m)
125+deriving instance PerformEvent t m => PerformEvent t (Input t m)
126+deriving instance PostBuild t m => PostBuild t (Input t m)
127+deriving instance TriggerEvent t m => TriggerEvent t (Input t m)
128+instance HasImageWriter t m => HasImageWriter t (Input t m)
129+instance HasDisplayRegion t m => HasDisplayRegion t (Input t m)
130+instance HasFocusReader t m => HasFocusReader t (Input t m)
131+
132+instance MonadTrans (Input t) where
133+ lift f = Input $ lift f
134+
135+instance MFunctor (Input t) where
136+ hoist f = Input . hoist f . unInput
137+
138+instance MonadNodeId m => MonadNodeId (Input t m)
139+
140+-- | Runs an 'Input' with a given context
141+runInput
142+ :: Reflex t
143+ => Event t VtyEvent
144+ -> Input t m a
145+ -> m a
146+runInput e w = runReaderT (unInput w) e
147+
148+-- ** Filtering input
149+
150+-- | Type synonym for a key and modifier combination
151+type KeyCombo = (V.Key, [V.Modifier])
152+
153+-- | Emits an event that fires on a particular key press (without modifiers)
154+key :: (Monad m, Reflex t, HasInput t m) => V.Key -> m (Event t KeyCombo)
155+key = keyCombos . Set.singleton . (,[])
156+
157+-- | Emits an event that fires on particular key presses (without modifiers)
158+keys :: (Monad m, Reflex t, HasInput t m) => [V.Key] -> m (Event t KeyCombo)
159+keys = keyCombos . Set.fromList . fmap (,[])
160+
161+-- | Emit an event that fires whenever the provided key combination occurs
162+keyCombo
163+ :: (Reflex t, Monad m, HasInput t m)
164+ => KeyCombo
165+ -> m (Event t KeyCombo)
166+keyCombo = keyCombos . Set.singleton
167+
168+-- | Emit an event that fires whenever any of the provided key combinations occur
169+keyCombos
170+ :: (Reflex t, Monad m, HasInput t m)
171+ => Set KeyCombo
172+ -> m (Event t KeyCombo)
173+keyCombos ks = do
174+ i <- input
175+ return $ fforMaybe i $ \case
176+ V.EvKey k m -> if Set.member (k, m) ks
177+ then Just (k, m)
178+ else Nothing
179+ _ -> Nothing
218180
219181-- | Filter the keyboard input that a child widget may receive
220−filterKeys :: (Reflex t, HasVtyInput t m) => (KeyCombo -> Bool) -> m a -> m a
182+filterKeys :: (Reflex t, HasInput t m) => (KeyCombo -> Bool) -> m a -> m a
221183filterKeys f x = localInput (ffilter (\case
222184 V.EvKey k mods -> f (k, mods)
223185 _ -> True)) x
224186
225−instance (Reflex t, Monad m) => HasVtyInput t (VtyWidget t m) where
226− input = VtyWidget . lift $ lift $ ask
227− localInput f (VtyWidget m) = VtyWidget $ hoist (hoist (local f)) m
187+-- | Filter mouse input events based on whether they target a particular region
188+-- and translate them to the internal coordinate system of that region.
189+--
190+-- NB: Non-mouse events are passed through unfiltered and unchanged
191+mouseInRegion :: Region -> VtyEvent -> Maybe VtyEvent
192+mouseInRegion (Region l t w h) e = case e of
193+ V.EvMouseDown x y btn m -> mouse (\u v -> V.EvMouseDown u v btn m) x y
194+ V.EvMouseUp x y btn -> mouse (\u v -> V.EvMouseUp u v btn) x y
195+ _ -> Just e
196+ where
197+ mouse con x y
198+ | or [ x < l
199+ , y < t
200+ , x >= l + w
201+ , y >= t + h ] = Nothing
202+ | otherwise =
203+ Just (con (x - l) (y - t))
204+
205+-- | Filter mouse input outside the current display region and
206+-- all input if the region is not focused
207+inputInFocusedRegion
208+ :: (HasDisplayRegion t m, HasFocusReader t m, HasInput t m)
209+ => m (Event t VtyEvent)
210+inputInFocusedRegion = do
211+ inp <- input
212+ reg <- current <$> askRegion
213+ foc <- current <$> focus
214+ pure $ fmapMaybe id $ attachWith filterInput ((,) <$> reg <*> foc) inp
215+ where
216+ filterInput (r, f) = \case
217+ V.EvKey {} | not f -> Nothing
218+ x -> mouseInRegion r x
219+
220+-- * Getting and setting the display region
221+
222+-- | A chunk of the display area
223+data Region = Region
224+ { _region_left :: Int
225+ , _region_top :: Int
226+ , _region_width :: Int
227+ , _region_height :: Int
228+ }
229+ deriving (Show, Read, Eq, Ord)
230+
231+-- | A region that occupies no space.
232+nilRegion :: Region
233+nilRegion = Region 0 0 0 0
234+
235+-- | The width and height of a 'Region'
236+regionSize :: Region -> (Int, Int)
237+regionSize (Region _ _ w h) = (w, h)
238+
239+-- | 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
243+
244+-- | A class for things that know their own display size dimensions
245+class (Reflex t, Monad m) => HasDisplayRegion t m | m -> t where
246+ -- | Retrieve the display region
247+ askRegion :: m (Dynamic t Region)
248+ default askRegion :: (f m' ~ m, MonadTrans f, HasDisplayRegion t m') => m (Dynamic t Region)
249+ askRegion = lift askRegion
250+ -- | Run an action in a local region, by applying a transformation to the region
251+ localRegion :: (Dynamic t Region -> Dynamic t Region) -> m a -> m a
252+ default localRegion :: (f m' ~ m, Monad m', MFunctor f, HasDisplayRegion t m') => (Dynamic t Region -> Dynamic t Region) -> m a -> m a
253+ localRegion f = hoist (localRegion f)
254+
255+-- | Retrieve the display width
256+displayWidth :: HasDisplayRegion t m => m (Dynamic t Int)
257+displayWidth = fmap _region_width <$> askRegion
258+
259+-- | Retrieve the display height
260+displayHeight :: HasDisplayRegion t m => m (Dynamic t Int)
261+displayHeight = fmap _region_height <$> askRegion
262+
263+instance HasDisplayRegion t m => HasDisplayRegion t (ReaderT x m)
264+instance HasDisplayRegion t m => HasDisplayRegion t (BehaviorWriterT t x m)
265+instance HasDisplayRegion t m => HasDisplayRegion t (DynamicWriterT t x m)
266+instance HasDisplayRegion t m => HasDisplayRegion t (EventWriterT t x m)
267+instance HasDisplayRegion t m => HasDisplayRegion t (NodeIdT m)
268+
269+-- | A widget that has access to a particular region of the vty display
270+newtype DisplayRegion t m a = DisplayRegion
271+ { unDisplayRegion :: ReaderT (Dynamic t Region) m a }
272+ deriving
273+ ( Functor
274+ , Applicative
275+ , Monad
276+ , MonadFix
277+ , MonadHold t
278+ , MonadIO
279+ , MonadRef
280+ , MonadSample t
281+ )
282+
283+instance (Monad m, Reflex t) => HasDisplayRegion t (DisplayRegion t m) where
284+ askRegion = DisplayRegion ask
285+ localRegion f = DisplayRegion . local f . unDisplayRegion
286+
287+deriving instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (DisplayRegion t m)
288+deriving instance NotReady t m => NotReady t (DisplayRegion t m)
289+deriving instance PerformEvent t m => PerformEvent t (DisplayRegion t m)
290+deriving instance PostBuild t m => PostBuild t (DisplayRegion t m)
291+deriving instance TriggerEvent t m => TriggerEvent t (DisplayRegion t m)
292+instance HasImageWriter t m => HasImageWriter t (DisplayRegion t m)
293+instance HasFocusReader t m => HasFocusReader t (DisplayRegion t m)
294+
295+instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (DisplayRegion t m) where
296+ runWithReplace (DisplayRegion a) e = DisplayRegion $ runWithReplace a $ fmap unDisplayRegion e
297+ traverseIntMapWithKeyWithAdjust f m e = DisplayRegion $ traverseIntMapWithKeyWithAdjust (\k v -> unDisplayRegion $ f k v) m e
298+ traverseDMapWithKeyWithAdjust f m e = DisplayRegion $ traverseDMapWithKeyWithAdjust (\k v -> unDisplayRegion $ f k v) m e
299+ traverseDMapWithKeyWithAdjustWithMove f m e = DisplayRegion $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unDisplayRegion $ f k v) m e
300+
301+instance MonadTrans (DisplayRegion t) where
302+ lift = DisplayRegion . lift
303+
304+instance MFunctor (DisplayRegion t) where
305+ hoist f = DisplayRegion . hoist f . unDisplayRegion
306+
307+instance MonadNodeId m => MonadNodeId (DisplayRegion t m)
308+
309+-- | Run a 'DisplayRegion' action with a given 'Region'
310+runDisplayRegion
311+ :: (Reflex t, Monad m)
312+ => Dynamic t Region
313+ -> DisplayRegion t m a
314+ -> m a
315+runDisplayRegion r = flip runReaderT r . unDisplayRegion
316+
317+-- * Getting focus state
228318
229319-- | A class for things that can dynamically gain and lose focus
230−class HasFocus t m | m -> t where
320+class (Reflex t, Monad m) => HasFocusReader t m | m -> t where
231321 focus :: m (Dynamic t Bool)
232− default focus :: (f m' ~ m, Monad m', MonadTrans f, HasFocus t m') => m (Dynamic t Bool)
322+ default focus :: (f m' ~ m, Monad m', MonadTrans f, HasFocusReader t m') => m (Dynamic t Bool)
233323 focus = lift focus
324+ localFocus :: (Dynamic t Bool -> Dynamic t Bool) -> m a -> m a
325+ default localFocus :: (f m' ~ m, Monad m', MFunctor f, HasFocusReader t m') => (Dynamic t Bool -> Dynamic t Bool) -> m a -> m a
326+ localFocus f = hoist (localFocus f)
327+
328+instance HasFocusReader t m => HasFocusReader t (ReaderT x m)
329+instance HasFocusReader t m => HasFocusReader t (BehaviorWriterT t x m)
330+instance HasFocusReader t m => HasFocusReader t (DynamicWriterT t x m)
331+instance HasFocusReader t m => HasFocusReader t (EventWriterT t x m)
332+instance HasFocusReader t m => HasFocusReader t (NodeIdT m)
333+
334+-- | A widget that has access to information about whether it is focused
335+newtype FocusReader t m a = FocusReader
336+ { unFocusReader :: ReaderT (Dynamic t Bool) m a }
337+ deriving
338+ ( Functor
339+ , Applicative
340+ , Monad
341+ , MonadFix
342+ , MonadHold t
343+ , MonadIO
344+ , MonadRef
345+ , MonadSample t
346+ )
347+
348+instance (Monad m, Reflex t) => HasFocusReader t (FocusReader t m) where
349+ focus = FocusReader ask
350+ localFocus f = FocusReader . local f . unFocusReader
351+
352+deriving instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (FocusReader t m)
353+deriving instance NotReady t m => NotReady t (FocusReader t m)
354+deriving instance PerformEvent t m => PerformEvent t (FocusReader t m)
355+deriving instance PostBuild t m => PostBuild t (FocusReader t m)
356+deriving instance TriggerEvent t m => TriggerEvent t (FocusReader t m)
357+instance HasImageWriter t m => HasImageWriter t (FocusReader t m)
234358
235−instance (Reflex t, Monad m) => HasFocus t (VtyWidget t m) where
236− focus = VtyWidget . lift $ asks _vtyWidgetCtx_focus
359+instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (FocusReader t m) where
360+ runWithReplace (FocusReader a) e = FocusReader $ runWithReplace a $ fmap unFocusReader e
361+ traverseIntMapWithKeyWithAdjust f m e = FocusReader $ traverseIntMapWithKeyWithAdjust (\k v -> unFocusReader $ f k v) m e
362+ traverseDMapWithKeyWithAdjust f m e = FocusReader $ traverseDMapWithKeyWithAdjust (\k v -> unFocusReader $ f k v) m e
363+ traverseDMapWithKeyWithAdjustWithMove f m e = FocusReader $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unFocusReader $ f k v) m e
364+
365+instance MonadTrans (FocusReader t) where
366+ lift = FocusReader . lift
367+
368+instance MFunctor (FocusReader t) where
369+ hoist f = FocusReader . hoist f . unFocusReader
370+
371+instance MonadNodeId m => MonadNodeId (FocusReader t m)
372+
373+-- | Run a 'FocusReader' action with the given focus value
374+runFocusReader
375+ :: (Reflex t, Monad m)
376+ => Dynamic t Bool
377+ -> FocusReader t m a
378+ -> m a
379+runFocusReader b = flip runReaderT b . unFocusReader
380+
381+-- * "Image" output
237382
238383-- | A class for widgets that can produce images to draw to the display
239−class (Reflex t, Monad m) => ImageWriter t m | m -> t where
384+class (Reflex t, Monad m) => HasImageWriter t m | m -> t where
240385 -- | Send images upstream for rendering
241386 tellImages :: Behavior t [Image] -> m ()
242− default tellImages :: (f m' ~ m, Monad m', MonadTrans f, ImageWriter t m') => Behavior t [Image] -> m ()
387+ default tellImages :: (f m' ~ m, Monad m', MonadTrans f, HasImageWriter t m') => Behavior t [Image] -> m ()
243388 tellImages = lift . tellImages
244389 -- | Apply a transformation to the images produced by the child actions
245390 mapImages :: (Behavior t [Image] -> Behavior t [Image]) -> m a -> m a
391+ default mapImages :: (f m' ~ m, Monad m', MFunctor f, HasImageWriter t m') => (Behavior t [Image] -> Behavior t [Image]) -> m a -> m a
392+ mapImages f = hoist (mapImages f)
393+
394+-- | A widget that can produce images to draw onto the display
395+newtype ImageWriter t m a = ImageWriter
396+ { unImageWriter :: BehaviorWriterT t [Image] m a }
397+ deriving
398+ ( Functor
399+ , Applicative
400+ , Monad
401+ , MonadFix
402+ , MonadHold t
403+ , MonadIO
404+ , MonadRef
405+ , MonadReflexCreateTrigger t
406+ , MonadSample t
407+ , NotReady t
408+ , PerformEvent t
409+ , PostBuild t
410+ , TriggerEvent t
411+ )
412+
413+instance MonadTrans (ImageWriter t) where
414+ lift = ImageWriter . lift
415+
416+instance MFunctor (ImageWriter t) where
417+ hoist f = ImageWriter . (hoist f) . unImageWriter
418+
419+instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (ImageWriter t m) where
420+ runWithReplace (ImageWriter a) e = ImageWriter $ runWithReplace a $ fmap unImageWriter e
421+ traverseIntMapWithKeyWithAdjust f m e = ImageWriter $ traverseIntMapWithKeyWithAdjust (\k v -> unImageWriter $ f k v) m e
422+ traverseDMapWithKeyWithAdjust f m e = ImageWriter $ traverseDMapWithKeyWithAdjust (\k v -> unImageWriter $ f k v) m e
423+ traverseDMapWithKeyWithAdjustWithMove f m e = ImageWriter $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unImageWriter $ f k v) m e
246424
247−instance (Monad m, Reflex t) => ImageWriter t (BehaviorWriterT t [Image] m) where
248− tellImages = tellBehavior
249− mapImages f x = do
425+instance HasImageWriter t m => HasImageWriter t (ReaderT x m)
426+instance HasImageWriter t m => HasImageWriter t (BehaviorWriterT t x m)
427+instance HasImageWriter t m => HasImageWriter t (DynamicWriterT t x m)
428+instance HasImageWriter t m => HasImageWriter t (EventWriterT t x m)
429+instance HasImageWriter t m => HasImageWriter t (NodeIdT m)
430+
431+instance (Monad m, Reflex t) => HasImageWriter t (ImageWriter t m) where
432+ tellImages = ImageWriter . tellBehavior
433+ mapImages f (ImageWriter x) = ImageWriter $ do
250434 (a, images) <- lift $ runBehaviorWriterT x
251− tellImages $ f images
435+ tellBehavior $ f images
252436 pure a
253437
254−-- | A chunk of the display area
255−data Region = Region
256− { _region_left :: Int
257− , _region_top :: Int
258− , _region_width :: Int
259− , _region_height :: Int
260− }
261− deriving (Show, Read, Eq, Ord)
438+instance HasDisplayRegion t m => HasDisplayRegion t (ImageWriter t m)
439+instance HasFocusReader t m => HasFocusReader t (ImageWriter t m)
262440
263−-- | The width and height of a 'Region'
264−regionSize :: Region -> (Int, Int)
265−regionSize (Region _ _ w h) = (w, h)
441+-- | Run a widget that can produce images
442+runImageWriter
443+ :: (Reflex t, Monad m)
444+ => ImageWriter t m a
445+ -> m (a, Behavior t [Image])
446+runImageWriter = runBehaviorWriterT . unImageWriter
447+
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+ )
266482
267−-- | Produces an 'Image' that fills a region with space characters
268−regionBlankImage :: Region -> Image
269−regionBlankImage r@(Region _ _ width height) =
270− withinImage r $ V.charFill V.defAttr ' ' width height
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+
517+-- ** Manipulating images
271518
272519-- | Translates and crops an 'Image' so that it is contained by
273520-- the given 'Region'.
@@ -279,7 +526,17 @@ withinImage (Region left top width height)
279526 | width < 0 || height < 0 = withinImage (Region left top 0 0)
280527 | otherwise = V.translate left top . V.crop width height
281528
282−-- | Low-level widget combinator that runs a child 'VtyWidget' within
529+-- | Crop a behavior of images to a behavior of regions. See 'withinImage'.
530+imagesInRegion
531+ :: Reflex t
532+ => Behavior t Region
533+ -> Behavior t [Image]
534+ -> Behavior t [Image]
535+imagesInRegion reg = liftA2 (\r is -> map (withinImage r) is) reg
536+
537+-- * Running sub-widgets
538+
539+-- | Low-level widget combinator that runs a child widget within
283540-- a given region and context. This widget filters and modifies the input
284541-- that the child widget receives such that:
285542-- * unfocused widgets receive no key events
@@ -287,433 +544,17 @@ withinImage (Region left top width height)
287544-- * mouse inputs inside the region have their coordinates translated such
288545-- that (0,0) is the top-left corner of the region
289546pane
290− :: (Reflex t, Monad m, MonadNodeId m, HasVtyWidgetCtx t m, HasVtyInput t m, ImageWriter t m)
547+ :: (Reflex t, Monad m, HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
291548 => Dynamic t Region
292549 -> Dynamic t Bool -- ^ Whether the widget should be focused when the parent is.
293550 -> m a
294551 -> m a
295−pane dr foc child = do
296− let reg = current dr
297− let subContext ctx = VtyWidgetCtx
298− { _vtyWidgetCtx_focus = liftA2 (&&) (_vtyWidgetCtx_focus ctx) foc
299− , _vtyWidgetCtx_width = _region_width <$> dr
300− , _vtyWidgetCtx_height = _region_height <$> dr
301− }
302− inputFilter = fmapMaybe id .
303− attachWith (\(r,f) e -> filterInput r f e)
304− (liftA2 (,) reg (current foc))
305− let imagesWithinRegion images = liftA2 (\is r -> map (withinImage r) is) images reg
306− mapImages imagesWithinRegion $ localInput inputFilter $ localCtx subContext child
307− where
308− filterInput :: Region -> Bool -> VtyEvent -> Maybe VtyEvent
309− filterInput (Region l t w h) focused e = case e of
310− V.EvKey _ _ | not focused -> Nothing
311− V.EvMouseDown x y btn m -> mouse (\u v -> V.EvMouseDown u v btn m) x y
312− V.EvMouseUp x y btn -> mouse (\u v -> V.EvMouseUp u v btn) x y
313− _ -> Just e
314− where
315− mouse con x y
316− | or [ x < l
317− , y < t
318− , x >= l + w
319− , y >= t + h ] = Nothing
320− | otherwise =
321− Just (con (x - l) (y - t))
322−
323−-- | Information about a drag operation
324−data Drag = Drag
325− { _drag_from :: (Int, Int) -- ^ Where the drag began
326− , _drag_to :: (Int, Int) -- ^ Where the mouse currently is
327− , _drag_button :: V.Button -- ^ Which mouse button is dragging
328− , _drag_modifiers :: [V.Modifier] -- ^ What modifiers are held
329− , _drag_end :: Bool -- ^ Whether the drag ended (the mouse button was released)
330− }
331− deriving (Eq, Ord, Show)
332−
333−-- | Converts raw vty mouse drag events into an event stream of 'Drag's
334−drag
335− :: (Reflex t, MonadFix m, MonadHold t m, HasVtyInput t m)
336− => V.Button
337− -> m (Event t Drag)
338−drag btn = do
339− inp <- input
340− let f :: Maybe Drag -> V.Event -> Maybe Drag
341− f Nothing = \case
342− V.EvMouseDown x y btn' mods
343− | btn == btn' -> Just $ Drag (x,y) (x,y) btn' mods False
344− | otherwise -> Nothing
345− _ -> Nothing
346− f (Just (Drag from _ _ mods end)) = \case
347− V.EvMouseDown x y btn' mods'
348− | end && btn == btn' -> Just $ Drag (x,y) (x,y) btn' mods' False
349− | btn == btn' -> Just $ Drag from (x,y) btn mods' False
350− | otherwise -> Nothing -- Ignore other buttons.
351− V.EvMouseUp x y (Just btn')
352− | end -> Nothing
353− | btn == btn' -> Just $ Drag from (x,y) btn mods True
354− | otherwise -> Nothing
355− V.EvMouseUp x y Nothing -- Terminal doesn't specify mouse up button,
356− -- assume it's the right one.
357− | end -> Nothing
358− | otherwise -> Just $ Drag from (x,y) btn mods True
359− _ -> Nothing
360− rec let newDrag = attachWithMaybe f (current dragD) inp
361− dragD <- holdDyn Nothing $ Just <$> newDrag
362− return (fmapMaybe id $ updated dragD)
363−
364−-- | Mouse down events for a particular mouse button
365−mouseDown
366− :: (Reflex t, Monad m, HasVtyInput t m)
367− => V.Button
368− -> m (Event t MouseDown)
369−mouseDown btn = do
370− i <- input
371− return $ fforMaybe i $ \case
372− V.EvMouseDown x y btn' mods -> if btn == btn'
373− then Just $ MouseDown btn' (x, y) mods
374− else Nothing
375− _ -> Nothing
376−
377−-- | Mouse up events for a particular mouse button
378−mouseUp
379− :: (Reflex t, Monad m, HasVtyInput t m)
380− => m (Event t MouseUp)
381−mouseUp = do
382− i <- input
383− return $ fforMaybe i $ \case
384− V.EvMouseUp x y btn' -> Just $ MouseUp btn' (x, y)
385− _ -> Nothing
386−
387−-- | Information about a mouse down event
388−data MouseDown = MouseDown
389− { _mouseDown_button :: V.Button
390− , _mouseDown_coordinates :: (Int, Int)
391− , _mouseDown_modifiers :: [V.Modifier]
392− }
393− deriving (Eq, Ord, Show)
394−
395−-- | Information about a mouse up event
396−data MouseUp = MouseUp
397− { _mouseUp_button :: Maybe V.Button
398− , _mouseUp_coordinates :: (Int, Int)
399− }
400− deriving (Eq, Ord, Show)
401−
402−-- | Mouse scroll direction
403−data ScrollDirection = ScrollDirection_Up | ScrollDirection_Down
404− deriving (Eq, Ord, Show)
405−
406−-- | Produce an event that fires when the mouse wheel is scrolled
407−mouseScroll
408− :: (Reflex t, Monad m, HasVtyInput t m)
409− => m (Event t ScrollDirection)
410−mouseScroll = do
411− up <- mouseDown V.BScrollUp
412− down <- mouseDown V.BScrollDown
413− return $ leftmost
414− [ ScrollDirection_Up <$ up
415− , ScrollDirection_Down <$ down
416− ]
417−
418−-- | Type synonym for a key and modifier combination
419−type KeyCombo = (V.Key, [V.Modifier])
420−
421−-- | Emits an event that fires on a particular key press (without modifiers)
422−key :: (Monad m, Reflex t, HasVtyInput t m) => V.Key -> m (Event t KeyCombo)
423−key = keyCombos . Set.singleton . (,[])
424−
425−-- | Emits an event that fires on particular key presses (without modifiers)
426−keys :: (Monad m, Reflex t, HasVtyInput t m) => [V.Key] -> m (Event t KeyCombo)
427−keys = keyCombos . Set.fromList . fmap (,[])
428−
429−-- | Emit an event that fires whenever the provided key combination occurs
430−keyCombo
431− :: (Reflex t, Monad m, HasVtyInput t m)
432− => KeyCombo
433− -> m (Event t KeyCombo)
434−keyCombo = keyCombos . Set.singleton
435−
436−-- | Emit an event that fires whenever any of the provided key combinations occur
437−keyCombos
438− :: (Reflex t, Monad m, HasVtyInput t m)
439− => Set KeyCombo
440− -> m (Event t KeyCombo)
441−keyCombos ks = do
442− i <- input
443− return $ fforMaybe i $ \case
444− V.EvKey k m -> if Set.member (k, m) ks
445− then Just (k, m)
446− else Nothing
447− _ -> Nothing
448−
449−-- | A plain split of the available space into vertically stacked panes.
450−-- No visual separator is built in here.
451−splitV :: (Reflex t, Monad m, MonadNodeId m, HasVtyWidgetCtx t m, HasDisplaySize t m, HasVtyInput t m, ImageWriter t m)
452− => Dynamic t (Int -> Int)
453− -- ^ Function used to determine size of first pane based on available size
454− -> Dynamic t (Bool, Bool)
455− -- ^ How to focus the two sub-panes, given that we are focused.
456− -> m a
457− -- ^ Widget for first pane
458− -> m b
459− -- ^ Widget for second pane
460− -> m (a,b)
461−splitV sizeFunD focD wA wB = do
462− dw <- displayWidth
463− dh <- displayHeight
464− let regA = Region 0 0 <$> dw <*> (sizeFunD <*> dh)
465− regB = Region 0 <$> (_region_height <$> regA) <*> dw <*> liftA2 (-) dh (_region_height <$> regA)
466− ra <- pane regA (fst <$> focD) wA
467− rb <- pane regB (snd <$> focD) wB
468− return (ra,rb)
469−
470−-- | A plain split of the available space into horizontally stacked panes.
471−-- No visual separator is built in here.
472−splitH :: (Reflex t, Monad m, MonadNodeId m, HasDisplaySize t m, HasVtyWidgetCtx t m, HasVtyInput t m, ImageWriter t m)
473− => Dynamic t (Int -> Int)
474− -- ^ Function used to determine size of first pane based on available size
475− -> Dynamic t (Bool, Bool)
476− -- ^ How to focus the two sub-panes, given that we are focused.
477− -> m a
478− -- ^ Widget for first pane
479− -> m b
480− -- ^ Widget for second pane
481− -> m (a,b)
482−splitH sizeFunD focD wA wB = do
483− dw <- displayWidth
484− dh <- displayHeight
485− let regA = Region 0 0 <$> (sizeFunD <*> dw) <*> dh
486− regB = Region <$> (_region_width <$> regA) <*> 0 <*> liftA2 (-) dw (_region_width <$> regA) <*> dh
487− liftA2 (,) (pane regA (fmap fst focD) wA) (pane regB (fmap snd focD) wB)
488−
489−-- | A split of the available space into two parts with a draggable separator.
490−-- Starts with half the space allocated to each, and the first pane has focus.
491−-- Clicking in a pane switches focus.
492−splitVDrag :: (Reflex t, MonadFix m, MonadHold t m, MonadNodeId m, HasDisplaySize t m, HasVtyInput t m, HasVtyWidgetCtx t m, ImageWriter t m)
493− => m ()
494− -> m a
495− -> m b
496− -> m (a,b)
497−splitVDrag wS wA wB = do
498− dh <- displayHeight
499− dw <- displayWidth
500− h0 <- sample $ current dh -- TODO
501− dragE <- drag V.BLeft
502− let splitter0 = h0 `div` 2
503− rec splitterCheckpoint <- holdDyn splitter0 $ leftmost [fst <$> ffilter snd dragSplitter, resizeSplitter]
504− splitterPos <- holdDyn splitter0 $ leftmost [fst <$> dragSplitter, resizeSplitter]
505− splitterFrac <- holdDyn ((1::Double) / 2) $ ffor (attach (current dh) (fst <$> dragSplitter)) $ \(h, x) ->
506− fromIntegral x / max 1 (fromIntegral h)
507− let dragSplitter = fforMaybe (attach (current splitterCheckpoint) dragE) $
508− \(splitterY, Drag (_, fromY) (_, toY) _ _ end) ->
509− if splitterY == fromY then Just (toY, end) else Nothing
510− regA = Region 0 0 <$> dw <*> splitterPos
511− regS = Region 0 <$> splitterPos <*> dw <*> 1
512− regB = Region 0 <$> (splitterPos + 1) <*> dw <*> (dh - splitterPos - 1)
513− resizeSplitter = ffor (attach (current splitterFrac) (updated dh)) $
514− \(frac, h) -> round (frac * fromIntegral h)
515− focA <- holdDyn True $ leftmost
516− [ True <$ mA
517− , False <$ mB
518− ]
519− (mA, rA) <- pane regA focA $ withMouseDown wA
520− pane regS (pure False) wS
521− (mB, rB) <- pane regB (not <$> focA) $ withMouseDown wB
522− return (rA, rB)
523− where
524− withMouseDown x = do
525− m <- mouseDown V.BLeft
526− x' <- x
527− return (m, x')
528−
529−-- | Fill the background with a particular character.
530−fill :: (HasDisplaySize t m, ImageWriter t m) => Char -> m ()
531−fill c = do
532− dw <- displayWidth
533− dh <- displayHeight
534− let fillImg = current $ liftA2 (\w h -> [V.charFill V.defAttr c w h]) dw dh
535− tellImages fillImg
536−
537−-- | Fill the background with the bottom
538−hRule :: (HasDisplaySize t m, ImageWriter t m) => BoxStyle -> m ()
539−hRule boxStyle = fill (_boxStyle_s boxStyle)
540−
541−-- | Defines a set of symbols to use to draw the outlines of boxes
542−-- C.f. https://en.wikipedia.org/wiki/Box-drawing_character
543−data BoxStyle = BoxStyle
544− { _boxStyle_nw :: Char
545− , _boxStyle_n :: Char
546− , _boxStyle_ne :: Char
547− , _boxStyle_e :: Char
548− , _boxStyle_se :: Char
549− , _boxStyle_s :: Char
550− , _boxStyle_sw :: Char
551− , _boxStyle_w :: Char
552− }
553−
554−instance Default BoxStyle where
555− def = singleBoxStyle
556−
557−-- | A box style that uses hyphens and pipe characters. Doesn't handle
558−-- corners very well.
559−hyphenBoxStyle :: BoxStyle
560−hyphenBoxStyle = BoxStyle '-' '-' '-' '|' '-' '-' '-' '|'
561−
562−-- | A single line box style
563−singleBoxStyle :: BoxStyle
564−singleBoxStyle = BoxStyle '┌' '─' '┐' '│' '┘' '─' '└' '│'
565−
566−-- | A thick single line box style
567−thickBoxStyle :: BoxStyle
568−thickBoxStyle = BoxStyle '┏' '━' '┓' '┃' '┛' '━' '┗' '┃'
569−
570−-- | A double line box style
571−doubleBoxStyle :: BoxStyle
572−doubleBoxStyle = BoxStyle '╔' '═' '╗' '║' '╝' '═' '╚' '║'
573−
574−-- | A single line box style with rounded corners
575−roundedBoxStyle :: BoxStyle
576−roundedBoxStyle = BoxStyle '╭' '─' '╮' '│' '╯' '─' '╰' '│'
577−
578−-- | Draws a titled box in the provided style and a child widget inside of that box
579−boxTitle :: (Monad m, Reflex t, MonadNodeId m, HasDisplaySize t m, ImageWriter t m, HasVtyWidgetCtx t m, HasVtyInput t m)
580− => Behavior t BoxStyle
581− -> Text
582− -> m a
583− -> m a
584−boxTitle boxStyle title child = do
585− dh <- displayHeight
586− dw <- displayWidth
587− let boxReg = Region 0 0 <$> dw <*> dh
588− innerReg = Region 1 1 <$> (subtract 2 <$> dw) <*> (subtract 2 <$> dh)
589− tellImages (boxImages <$> boxStyle <*> current boxReg)
590− tellImages (fmap (\r -> [regionBlankImage r]) (current innerReg))
591− pane innerReg (pure True) child
592− where
593− boxImages :: BoxStyle -> Region -> [Image]
594− boxImages style (Region left top width height) =
595− let right = left + width - 1
596− bottom = top + height - 1
597− sides =
598− [ withinImage (Region (left + 1) top (width - 2) 1) $
599− V.text' V.defAttr $
600− hPadText title (_boxStyle_n style) (width - 2)
601− , withinImage (Region right (top + 1) 1 (height - 2)) $
602− V.charFill V.defAttr (_boxStyle_e style) 1 (height - 2)
603− , withinImage (Region (left + 1) bottom (width - 2) 1) $
604− V.charFill V.defAttr (_boxStyle_s style) (width - 2) 1
605− , withinImage (Region left (top + 1) 1 (height - 2)) $
606− V.charFill V.defAttr (_boxStyle_w style) 1 (height - 2)
607− ]
608− corners =
609− [ withinImage (Region left top 1 1) $
610− V.char V.defAttr (_boxStyle_nw style)
611− , withinImage (Region right top 1 1) $
612− V.char V.defAttr (_boxStyle_ne style)
613− , withinImage (Region right bottom 1 1) $
614− V.char V.defAttr (_boxStyle_se style)
615− , withinImage (Region left bottom 1 1) $
616− V.char V.defAttr (_boxStyle_sw style)
617− ]
618− in sides ++ if width > 1 && height > 1 then corners else []
619− hPadText :: T.Text -> Char -> Int -> T.Text
620− hPadText t c l = if lt >= l
621− then t
622− else left <> t <> right
623− where
624− lt = T.length t
625− delta = l - lt
626− mkHalf n = T.replicate (n `div` 2) (T.singleton c)
627− left = mkHalf $ delta + 1
628− right = mkHalf delta
629−
630−-- | A box without a title
631−box :: (Monad m, Reflex t, MonadNodeId m, HasDisplaySize t m, ImageWriter t m, HasVtyWidgetCtx t m, HasVtyInput t m)
632− => Behavior t BoxStyle
633− -> m a
634− -> m a
635−box boxStyle = boxTitle boxStyle mempty
636−
637−-- | A box whose style is static
638−boxStatic
639− :: (Monad m, Reflex t, MonadNodeId m, HasDisplaySize t m, ImageWriter t m, HasVtyWidgetCtx t m, HasVtyInput t m)
640− => BoxStyle
641− -> m a
642− -> m a
643−boxStatic = box . pure
552+pane dr foc child = localRegion (const dr) $
553+ mapImages (imagesInRegion $ current dr) $
554+ localFocus (const foc) $
555+ inputInFocusedRegion >>= \e -> localInput (const e) child
644556
645−-- | Configuration options for displaying "rich" text
646−data RichTextConfig t = RichTextConfig
647− { _richTextConfig_attributes :: Behavior t V.Attr
648− }
649−
650−instance Reflex t => Default (RichTextConfig t) where
651− def = RichTextConfig $ pure V.defAttr
652−
653−-- | A widget that displays text with custom time-varying attributes
654−richText
655− :: (Reflex t, Monad m, HasDisplaySize t m, ImageWriter t m)
656− => RichTextConfig t
657− -> Behavior t Text
658− -> m ()
659−richText cfg t = do
660− dw <- displayWidth
661− let img = (\w a s -> [wrapText w a s])
662− <$> current dw
663− <*> _richTextConfig_attributes cfg
664− <*> t
665− tellImages img
666− where
667− wrapText maxWidth attrs = V.vertCat
668− . concatMap (fmap (V.string attrs . T.unpack) . TZ.wrapWithOffset maxWidth 0)
669− . T.split (=='\n')
670−
671−-- | Renders text, wrapped to the container width
672−text
673− :: (Reflex t, Monad m, HasDisplaySize t m, ImageWriter t m)
674− => Behavior t Text
675− -> m ()
676−text = richText def
677−
678−-- | Scrollable text widget. The output pair exposes the current scroll position and total number of lines (including those
679−-- that are hidden)
680−scrollableText
681− :: forall t m. (Reflex t, MonadHold t m, MonadFix m, HasDisplaySize t m, HasVtyInput t m, ImageWriter t m)
682− => Event t Int
683− -- ^ Number of lines to scroll by
684− -> Behavior t Text
685− -> m (Behavior t (Int, Int))
686− -- ^ (Current scroll position, total number of lines)
687−scrollableText scrollBy t = do
688− dw <- displayWidth
689− let imgs = wrap <$> current dw <*> t
690− kup <- key V.KUp
691− kdown <- key V.KDown
692− m <- mouseScroll
693− let requestedScroll :: Event t Int
694− requestedScroll = leftmost
695− [ 1 <$ kdown
696− , (-1) <$ kup
697− , ffor m $ \case
698− ScrollDirection_Up -> (-1)
699− ScrollDirection_Down -> 1
700− , scrollBy
701− ]
702− updateLine maxN delta ix = min (max 0 (ix + delta)) maxN
703− lineIndex :: Dynamic t Int <- foldDyn (\(maxN, delta) ix -> updateLine (maxN - 1) delta ix) 0 $
704− attach (length <$> imgs) requestedScroll
705− tellImages $ fmap ((:[]) . V.vertCat) $ drop <$> current lineIndex <*> imgs
706− return $ (,) <$> ((+) <$> current lineIndex <*> pure 1) <*> (length <$> imgs)
707− where
708− wrap maxWidth = concatMap (fmap (V.string V.defAttr . T.unpack) . TZ.wrapWithOffset maxWidth 0) . T.split (=='\n')
709−
710−-- | Renders any behavior whose value can be converted to
711−-- 'String' as text
712−display
713− :: (Reflex t, Monad m, Show a, HasDisplaySize t m, ImageWriter t m)
714− => Behavior t a
715− -> m ()
716−display a = text $ T.pack . show <$> a
557+-- * Misc
717558
718559-- | A widget that draws nothing
719560blank :: Monad m => m ()
src/Reflex/Vty/Widget/Box.hsnew file+124
@@ -0,0 +1,124 @@
1+{-|
2+ Description: Drawing boxes in various styles
3+-}
4+module Reflex.Vty.Widget.Box where
5+
6+import Data.Default
7+import Data.Text (Text)
8+import qualified Data.Text as T
9+import Graphics.Vty (Image)
10+import qualified Graphics.Vty as V
11+import Reflex
12+import Reflex.Vty.Widget
13+import Reflex.Vty.Widget.Text
14+
15+-- | Fill the background with the bottom box style
16+hRule :: (HasDisplayRegion t m, HasImageWriter t m, HasTheme t m) => BoxStyle -> m ()
17+hRule boxStyle = fill $ pure (_boxStyle_s boxStyle)
18+
19+-- | Defines a set of symbols to use to draw the outlines of boxes
20+-- C.f. https://en.wikipedia.org/wiki/Box-drawing_character
21+data BoxStyle = BoxStyle
22+ { _boxStyle_nw :: Char
23+ , _boxStyle_n :: Char
24+ , _boxStyle_ne :: Char
25+ , _boxStyle_e :: Char
26+ , _boxStyle_se :: Char
27+ , _boxStyle_s :: Char
28+ , _boxStyle_sw :: Char
29+ , _boxStyle_w :: Char
30+ }
31+
32+instance Default BoxStyle where
33+ def = singleBoxStyle
34+
35+-- | A box style that uses hyphens and pipe characters. Doesn't handle
36+-- corners very well.
37+hyphenBoxStyle :: BoxStyle
38+hyphenBoxStyle = BoxStyle '-' '-' '-' '|' '-' '-' '-' '|'
39+
40+-- | A single line box style
41+singleBoxStyle :: BoxStyle
42+singleBoxStyle = BoxStyle '┌' '─' '┐' '│' '┘' '─' '└' '│'
43+
44+-- | A thick single line box style
45+thickBoxStyle :: BoxStyle
46+thickBoxStyle = BoxStyle '┏' '━' '┓' '┃' '┛' '━' '┗' '┃'
47+
48+-- | A double line box style
49+doubleBoxStyle :: BoxStyle
50+doubleBoxStyle = BoxStyle '╔' '═' '╗' '║' '╝' '═' '╚' '║'
51+
52+-- | A single line box style with rounded corners
53+roundedBoxStyle :: BoxStyle
54+roundedBoxStyle = BoxStyle '╭' '─' '╮' '│' '╯' '─' '╰' '│'
55+
56+-- | 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+ => Behavior t BoxStyle
59+ -> Behavior t Text
60+ -> m a
61+ -> m a
62+boxTitle boxStyle title child = do
63+ dh <- displayHeight
64+ dw <- displayWidth
65+ bt <- theme
66+ let boxReg = Region 0 0 <$> dw <*> dh
67+ 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+
72+ pane innerReg (pure True) child
73+ where
74+ boxImages :: V.Attr -> Text -> BoxStyle -> Region -> [Image]
75+ boxImages attr title' style (Region left top width height) =
76+ let right = left + width - 1
77+ bottom = top + height - 1
78+ sides =
79+ [ withinImage (Region (left + 1) top (width - 2) 1) $
80+ V.text' attr $
81+ hPadText title' (_boxStyle_n style) (width - 2)
82+ , withinImage (Region right (top + 1) 1 (height - 2)) $
83+ V.charFill attr (_boxStyle_e style) 1 (height - 2)
84+ , withinImage (Region (left + 1) bottom (width - 2) 1) $
85+ V.charFill attr (_boxStyle_s style) (width - 2) 1
86+ , withinImage (Region left (top + 1) 1 (height - 2)) $
87+ V.charFill attr (_boxStyle_w style) 1 (height - 2)
88+ ]
89+ corners =
90+ [ withinImage (Region left top 1 1) $
91+ V.char attr (_boxStyle_nw style)
92+ , withinImage (Region right top 1 1) $
93+ V.char attr (_boxStyle_ne style)
94+ , withinImage (Region right bottom 1 1) $
95+ V.char attr (_boxStyle_se style)
96+ , withinImage (Region left bottom 1 1) $
97+ V.char attr (_boxStyle_sw style)
98+ ]
99+ in sides ++ if width > 1 && height > 1 then corners else []
100+ hPadText :: T.Text -> Char -> Int -> T.Text
101+ hPadText t c l = if lt >= l
102+ then t
103+ else left <> t <> right
104+ where
105+ lt = T.length t
106+ delta = l - lt
107+ mkHalf n = T.replicate (n `div` 2) (T.singleton c)
108+ left = mkHalf $ delta + 1
109+ right = mkHalf delta
110+
111+-- | 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)
113+ => Behavior t BoxStyle
114+ -> m a
115+ -> m a
116+box boxStyle = boxTitle boxStyle mempty
117+
118+-- | A box whose style is static
119+boxStatic
120+ :: (Monad m, Reflex t, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m, HasTheme t m)
121+ => BoxStyle
122+ -> m a
123+ -> m a
124+boxStatic = box . pure
src/Reflex/Vty/Widget/Input.hs+18−8
@@ -8,16 +8,20 @@ module Reflex.Vty.Widget.Input
88 ) where
99
1010
11+import Reflex.Vty.Widget.Input.Mouse as Export
1112import Reflex.Vty.Widget.Input.Text as Export
1213
1314import Control.Monad (join)
1415import Control.Monad.Fix (MonadFix)
15−import Control.Monad.NodeId (MonadNodeId)
1616import Data.Default (Default(..))
1717import Data.Text (Text)
1818import qualified Graphics.Vty as V
1919import Reflex
2020import Reflex.Vty.Widget
21+import Reflex.Vty.Widget.Box
22+import Reflex.Vty.Widget.Text
23+
24+-- * Buttons
2125
2226-- | Configuration options for the 'button' widget
2327data ButtonConfig t = ButtonConfig
@@ -30,7 +34,7 @@ instance Reflex t => Default (ButtonConfig t) where
3034
3135-- | A button widget that contains a sub-widget
3236button
33− :: (Reflex t, Monad m, MonadNodeId m, HasFocus t m, HasDisplaySize t m, ImageWriter t m, HasVtyWidgetCtx t m, HasVtyInput t m)
37+ :: (Reflex t, Monad m, HasFocusReader t m, HasTheme t m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m)
3438 => ButtonConfig t
3539 -> m ()
3640 -> m (Event t ())
@@ -48,7 +52,7 @@ button cfg child = do
4852
4953-- | A button widget that displays text that can change
5054textButton
51− :: (Reflex t, Monad m, MonadNodeId m, HasDisplaySize t m, HasFocus t m, ImageWriter t m, HasVtyWidgetCtx t m, HasVtyInput t m)
55+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasFocusReader t m, HasTheme t m, HasImageWriter t m, HasInput t m)
5256 => ButtonConfig t
5357 -> Behavior t Text
5458 -> m (Event t ())
@@ -56,31 +60,36 @@ textButton cfg = button cfg . text -- TODO Centering etc.
5660
5761-- | A button widget that displays a static bit of text
5862textButtonStatic
59− :: (Reflex t, Monad m, MonadNodeId m, HasDisplaySize t m, HasFocus t m, ImageWriter t m, HasVtyWidgetCtx t m, HasVtyInput t m)
63+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasFocusReader t m, HasTheme t m, HasImageWriter t m, HasInput t m)
6064 => ButtonConfig t
6165 -> Text
6266 -> m (Event t ())
6367textButtonStatic cfg = textButton cfg . pure
6468
69+-- * Links
70+
6571-- | A clickable link widget
6672link
67− :: (Reflex t, Monad m, HasDisplaySize t m, ImageWriter t m, HasVtyInput t m)
73+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasTheme t m)
6874 => Behavior t Text
6975 -> m (Event t MouseUp)
7076link t = do
77+ bt <- theme
7178 let cfg = RichTextConfig
72− { _richTextConfig_attributes = pure $ V.withStyle V.defAttr V.underline
79+ { _richTextConfig_attributes = fmap (\attr -> V.withStyle attr V.underline) bt
7380 }
7481 richText cfg t
7582 mouseUp
7683
7784-- | A clickable link widget with a static label
7885linkStatic
79− :: (Reflex t, Monad m, ImageWriter t m, HasDisplaySize t m, HasVtyInput t m)
86+ :: (Reflex t, Monad m, HasImageWriter t m, HasDisplayRegion t m, HasInput t m, HasTheme t m)
8087 => Text
8188 -> m (Event t MouseUp)
8289linkStatic = link . pure
8390
91+-- * Checkboxes
92+
8493-- | Characters used to render checked and unchecked textboxes
8594data CheckboxStyle = CheckboxStyle
8695 { _checkboxStyle_unchecked :: Text
@@ -107,6 +116,7 @@ checkboxStyleTick = CheckboxStyle
107116-- | Configuration options for a checkbox
108117data CheckboxConfig t = CheckboxConfig
109118 { _checkboxConfig_checkboxStyle :: Behavior t CheckboxStyle
119+ -- TODO DELETE and use HasTheme instead
110120 , _checkboxConfig_attributes :: Behavior t V.Attr
111121 , _checkboxConfig_setValue :: Event t Bool
112122 }
@@ -120,7 +130,7 @@ instance (Reflex t) => Default (CheckboxConfig t) where
120130
121131-- | A checkbox widget
122132checkbox
123− :: (MonadHold t m, MonadFix m, Reflex t, HasVtyInput t m, HasDisplaySize t m, ImageWriter t m, HasFocus t m)
133+ :: (MonadHold t m, MonadFix m, Reflex t, HasInput t m, HasDisplayRegion t m, HasImageWriter t m, HasFocusReader t m, HasTheme t m)
124134 => CheckboxConfig t
125135 -> Bool
126136 -> m (Dynamic t Bool)
src/Reflex/Vty/Widget/Input/Mouse.hsnew file+106
@@ -0,0 +1,106 @@
1+{-|
2+ Description: Mouse clicks, drags, and scrolls
3+-}
4+module Reflex.Vty.Widget.Input.Mouse where
5+
6+import Control.Monad.Fix
7+import qualified Graphics.Vty as V
8+import Reflex
9+import Reflex.Vty.Widget
10+
11+-- | Information about a drag operation
12+data Drag = Drag
13+ { _drag_from :: (Int, Int) -- ^ Where the drag began
14+ , _drag_to :: (Int, Int) -- ^ Where the mouse currently is
15+ , _drag_button :: V.Button -- ^ Which mouse button is dragging
16+ , _drag_modifiers :: [V.Modifier] -- ^ What modifiers are held
17+ , _drag_end :: Bool -- ^ Whether the drag ended (the mouse button was released)
18+ }
19+ deriving (Eq, Ord, Show)
20+
21+-- | Converts raw vty mouse drag events into an event stream of 'Drag's
22+drag
23+ :: (Reflex t, MonadFix m, MonadHold t m, HasInput t m)
24+ => V.Button
25+ -> m (Event t Drag)
26+drag btn = do
27+ inp <- input
28+ let f :: Maybe Drag -> V.Event -> Maybe Drag
29+ f Nothing = \case
30+ V.EvMouseDown x y btn' mods
31+ | btn == btn' -> Just $ Drag (x,y) (x,y) btn' mods False
32+ | otherwise -> Nothing
33+ _ -> Nothing
34+ f (Just (Drag from _ _ mods end)) = \case
35+ V.EvMouseDown x y btn' mods'
36+ | end && btn == btn' -> Just $ Drag (x,y) (x,y) btn' mods' False
37+ | btn == btn' -> Just $ Drag from (x,y) btn mods' False
38+ | otherwise -> Nothing -- Ignore other buttons.
39+ V.EvMouseUp x y (Just btn')
40+ | end -> Nothing
41+ | btn == btn' -> Just $ Drag from (x,y) btn mods True
42+ | otherwise -> Nothing
43+ V.EvMouseUp x y Nothing -- Terminal doesn't specify mouse up button,
44+ -- assume it's the right one.
45+ | end -> Nothing
46+ | otherwise -> Just $ Drag from (x,y) btn mods True
47+ _ -> Nothing
48+ rec let newDrag = attachWithMaybe f (current dragD) inp
49+ dragD <- holdDyn Nothing $ Just <$> newDrag
50+ return (fmapMaybe id $ updated dragD)
51+
52+-- | Mouse down events for a particular mouse button
53+mouseDown
54+ :: (Reflex t, Monad m, HasInput t m)
55+ => V.Button
56+ -> m (Event t MouseDown)
57+mouseDown btn = do
58+ i <- input
59+ return $ fforMaybe i $ \case
60+ V.EvMouseDown x y btn' mods -> if btn == btn'
61+ then Just $ MouseDown btn' (x, y) mods
62+ else Nothing
63+ _ -> Nothing
64+
65+-- | Mouse up events for a particular mouse button
66+mouseUp
67+ :: (Reflex t, Monad m, HasInput t m)
68+ => m (Event t MouseUp)
69+mouseUp = do
70+ i <- input
71+ return $ fforMaybe i $ \case
72+ V.EvMouseUp x y btn' -> Just $ MouseUp btn' (x, y)
73+ _ -> Nothing
74+
75+-- | Information about a mouse down event
76+data MouseDown = MouseDown
77+ { _mouseDown_button :: V.Button
78+ , _mouseDown_coordinates :: (Int, Int)
79+ , _mouseDown_modifiers :: [V.Modifier]
80+ }
81+ deriving (Eq, Ord, Show)
82+
83+-- | Information about a mouse up event
84+data MouseUp = MouseUp
85+ { _mouseUp_button :: Maybe V.Button
86+ , _mouseUp_coordinates :: (Int, Int)
87+ }
88+ deriving (Eq, Ord, Show)
89+
90+-- | Mouse scroll direction
91+data ScrollDirection = ScrollDirection_Up | ScrollDirection_Down
92+ deriving (Eq, Ord, Show)
93+
94+-- | Produce an event that fires when the mouse wheel is scrolled
95+mouseScroll
96+ :: (Reflex t, Monad m, HasInput t m)
97+ => m (Event t ScrollDirection)
98+mouseScroll = do
99+ up <- mouseDown V.BScrollUp
100+ down <- mouseDown V.BScrollDown
101+ return $ leftmost
102+ [ ScrollDirection_Up <$ up
103+ , ScrollDirection_Down <$ down
104+ ]
105+
106+
src/Reflex/Vty/Widget/Input/Text.hs+17−12
@@ -9,7 +9,6 @@ module Reflex.Vty.Widget.Input.Text
99
1010import Control.Monad (join)
1111import Control.Monad.Fix (MonadFix)
12−import Control.Monad.NodeId (MonadNodeId)
1312import Data.Default (Default(..))
1413import Data.Text (Text)
1514import Data.Text.Zipper
@@ -18,6 +17,7 @@ import Reflex
1817
1918import Reflex.Vty.Widget
2019import Reflex.Vty.Widget.Layout
20+import Reflex.Vty.Widget.Input.Mouse
2121
2222-- | Configuration options for a 'textInput'. For more information on
2323-- 'TextZipper', see 'Data.Text.Zipper'.
@@ -43,7 +43,7 @@ data TextInput t = TextInput
4343
4444-- | A widget that allows text input
4545textInput
46− :: (Reflex t, MonadHold t m, MonadFix m, HasVtyInput t m, HasFocus t m, HasDisplaySize t m, ImageWriter t m)
46+ :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m, HasFocusReader t m, HasTheme t m, HasDisplayRegion t m, HasImageWriter t m, HasDisplayRegion t m)
4747 => TextInputConfig t
4848 -> m (TextInput t)
4949textInput cfg = do
@@ -51,6 +51,8 @@ textInput cfg = do
5151 f <- focus
5252 dh <- displayHeight
5353 dw <- displayWidth
54+ bt <- theme
55+ attr0 <- sample bt
5456 rec v <- foldDyn ($) (_textInputConfig_initialValue cfg) $ mergeWith (.)
5557 [ uncurry (updateTextZipper (_textInputConfig_tabWidth cfg)) <$> attach (current dh) i
5658 , _textInputConfig_modify cfg
@@ -59,13 +61,20 @@ textInput cfg = do
5961 goToDisplayLinePosition mx (st + my) dl
6062 ]
6163 click <- mouseDown V.BLeft
62− let cursorAttrs = ffor f $ \x -> if x then cursorAttributes else V.defAttr
63− let rows = (\w s c -> displayLines w V.defAttr c s)
64+
65+ -- TODO reverseVideo is prob not what we want. Does not work with `darkTheme` in example.hs (cursor is dark rather than light bg)
66+ let toCursorAttrs attr = V.withStyle attr V.reverseVideo
67+ rowInputDyn = (,,)
6468 <$> dw
6569 <*> (mapZipper <$> _textInputConfig_display cfg <*> v)
66− <*> cursorAttrs
70+ <*> f
71+ toDisplayLines attr (w, s, x) =
72+ let c = if x then toCursorAttrs attr else attr
73+ in displayLines w attr c s
74+ attrDyn <- holdDyn attr0 $ pushAlways (\_ -> sample bt) (updated rowInputDyn)
75+ let rows = ffor2 attrDyn rowInputDyn toDisplayLines
6776 img = images . _displayLines_spans <$> rows
68− y <- holdUniqDyn $ _displayLines_cursorY <$> rows
77+ y <- holdUniqDyn $ fmap snd _displayLines_cursorPos <$> rows
6978 let newScrollTop :: Int -> (Int, Int) -> Int
7079 newScrollTop st (h, cursorY)
7180 | cursorY < st = cursorY
@@ -81,7 +90,7 @@ textInput cfg = do
8190
8291-- | A widget that allows multiline text input
8392multilineTextInput
84− :: (Reflex t, MonadHold t m, MonadFix m, HasVtyInput t m, HasFocus t m, HasDisplaySize t m, ImageWriter t m)
93+ :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m, HasFocusReader t m, HasTheme t m, HasDisplayRegion t m, HasImageWriter t m)
8594 => TextInputConfig t
8695 -> m (TextInput t)
8796multilineTextInput cfg = do
@@ -99,7 +108,7 @@ multilineTextInput cfg = do
99108-- the computed line count to greedily size the tile when vertically
100109-- oriented, and uses the fallback width when horizontally oriented.
101110textInputTile
102− :: (Monad m, MonadNodeId m, Reflex t, MonadFix m, MonadLayout t m, HasVtyWidgetCtx t m, HasVtyInput t m, MonadFocus t m, ImageWriter t m)
111+ :: (Monad m, Reflex t, MonadFix m, HasLayout t m, HasInput t m, HasFocus t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m, HasTheme t m)
103112 => m (TextInput t)
104113 -> Dynamic t Int
105114 -> m (TextInput t)
@@ -111,10 +120,6 @@ textInputTile txt width = do
111120 Orientation_Row -> width
112121 return t
113122
114−-- | Default attributes for the text cursor
115−cursorAttributes :: V.Attr
116−cursorAttributes = V.withStyle V.defAttr V.reverseVideo
117−
118123-- | Turn a set of display line rows into a list of images (one per line)
119124images :: [[Span V.Attr]] -> [V.Image]
120125images = map (V.horizCat . map spanToImage)
src/Reflex/Vty/Widget/Layout.hs+45−50
@@ -13,7 +13,7 @@ import Control.Monad.Reader
1313import Data.List (mapAccumL)
1414import Data.Map.Ordered (OMap)
1515import qualified Data.Map.Ordered as OMap
16−import Data.Maybe (fromMaybe)
16+import Data.Maybe (fromMaybe, isNothing)
1717import Data.Ratio ((%))
1818import Data.Semigroup (First(..))
1919import Data.Set.Ordered (OSet)
@@ -23,6 +23,7 @@ import qualified Graphics.Vty as V
2323import Reflex
2424import Reflex.Host.Class (MonadReflexCreateTrigger)
2525import Reflex.Vty.Widget
26+import Reflex.Vty.Widget.Input.Mouse
2627
2728-- * Focus
2829--
@@ -34,7 +35,7 @@ import Reflex.Vty.Widget
3435-- a tab press in a sibling element, a click event).
3536--
3637-- Focusable elements will usually be created via 'tile', but can also be
37−-- constructed via 'makeFocus' in 'MonadFocus'. The latter option allows for
38+-- constructed via 'makeFocus' in 'HasFocus'. The latter option allows for
3839-- more find-grained control of focus behavior.
3940
4041-- ** Storing focus state
@@ -75,7 +76,7 @@ shiftFS (FocusSet s) fid n = case OSet.findIndex <$> fid <*> pure s of
7576-- ** The focus management monad
7677
7778-- | A class for things that can produce focusable elements.
78−class (Monad m, Reflex t) => MonadFocus t m | m -> t where
79+class (Monad m, Reflex t) => HasFocus t m | m -> t where
7980 -- | Create a focusable element.
8081 makeFocus :: m FocusId
8182 -- | Emit an 'Event' of requests to change the focus.
@@ -105,7 +106,7 @@ newtype Focus t m a = Focus
105106 , PerformEvent t
106107 , NotReady t
107108 , MonadReflexCreateTrigger t
108− , HasDisplaySize t
109+ , HasDisplayRegion t
109110 , PostBuild t
110111 , MonadNodeId
111112 , MonadIO
@@ -124,18 +125,17 @@ instance (Adjustable t m, MonadHold t m, MonadFix m) => Adjustable t (Focus t m)
124125 traverseDMapWithKeyWithAdjust f m e = Focus $ traverseDMapWithKeyWithAdjust (\k v -> unFocus $ f k v) m e
125126 traverseDMapWithKeyWithAdjustWithMove f m e = Focus $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unFocus $ f k v) m e
126127
127−instance (Reflex t, MonadFix m, HasVtyInput t m) => HasVtyInput t (Focus t m) where
128+instance (Reflex t, MonadFix m, HasInput t m) => HasInput t (Focus t m) where
128129 localInput f = hoist (localInput f)
129130
130−instance (HasVtyWidgetCtx t m, Reflex t, MonadFix m) => HasVtyWidgetCtx t (Focus t m) where
131− localCtx f = hoist (localCtx f)
132−
133−instance (ImageWriter t m, MonadFix m) => ImageWriter t (Focus t m) where
131+instance (HasImageWriter t m, MonadFix m) => HasImageWriter t (Focus t m) where
134132 mapImages f = hoist (mapImages f)
135133
136−instance (HasFocus t m, Monad m) => HasFocus t (Focus t m)
134+instance (HasFocusReader t m, Monad m) => HasFocusReader t (Focus t m)
135+
136+instance (HasTheme t m, Monad m) => HasTheme t (Focus t m)
137137
138−instance (Reflex t, MonadFix m, MonadNodeId m) => MonadFocus t (Focus t m) where
138+instance (Reflex t, MonadFix m, MonadNodeId m) => HasFocus t (Focus t m) where
139139 makeFocus = do
140140 fid <- FocusId <$> lift getNextNodeId
141141 Focus $ tellDyn $ pure $ singletonFS fid
@@ -164,12 +164,14 @@ runFocus (Focus x) = do
164164 f (fs, rf) mf = case getFirst rf of
165165 Refocus_Clear -> Nothing
166166 Refocus_Id fid -> Just fid
167− Refocus_Shift n -> shiftFS fs mf n
167+ Refocus_Shift n -> if n < 0 && isNothing mf
168+ then shiftFS fs (OSet.elemAt (unFocusSet fs) 0) n
169+ else shiftFS fs mf n
168170
169171-- | Runs an action in the focus monad, providing it with information about
170172-- whether any of the foci created within it are focused.
171173anyChildFocused
172− :: (MonadFocus t m, MonadFix m)
174+ :: (HasFocus t m, MonadFix m)
173175 => (Dynamic t Bool -> m a)
174176 -> m a
175177anyChildFocused f = do
@@ -184,7 +186,7 @@ anyChildFocused f = do
184186
185187-- | Request focus be shifted backward and forward based on tab presses. <Tab>
186188-- shifts focus forward and <Shift+Tab> shifts focus backward.
187−tabNavigation :: (Reflex t, MonadNodeId m, HasVtyInput t m, MonadFocus t m) => m ()
189+tabNavigation :: (Reflex t, HasInput t m, HasFocus t m) => m ()
188190tabNavigation = do
189191 fwd <- fmap (const 1) <$> key (V.KChar '\t')
190192 back <- fmap (const (-1)) <$> key V.KBackTab
@@ -242,14 +244,14 @@ data Orientation = Orientation_Column
242244
243245-- | Create a row-oriented 'axis'
244246row
245− :: (Reflex t, MonadNodeId m, MonadFix m, MonadLayout t m)
247+ :: (Reflex t, MonadFix m, HasLayout t m)
246248 => m a
247249 -> m a
248250row = axis (pure Orientation_Row) flex
249251
250252-- | Create a column-oriented 'axis'
251253col
252− :: (Reflex t, MonadNodeId m, MonadFix m, MonadLayout t m)
254+ :: (Reflex t, MonadFix m, HasLayout t m)
253255 => m a
254256 -> m a
255257col = axis (pure Orientation_Column) flex
@@ -360,8 +362,8 @@ chunk o r (offset, sz) = case o of
360362-- ** The layout monad
361363
362364-- | A class of operations for creating screen layouts.
363−class Monad m => MonadLayout t m | m -> t where
364− -- | Creates a parent element in the current layout with the given size
365+class Monad m => HasLayout t m | m -> t where
366+ -- | Starts a parent element in the current layout with the given size
365367 -- constraint, which lays out its children according to the provided
366368 -- orientation.
367369 axis :: Dynamic t Orientation -> Dynamic t Constraint -> m a -> m a
@@ -380,18 +382,18 @@ newtype Layout t m a = Layout
380382 deriving
381383 ( Functor
382384 , Applicative
385+ , HasDisplayRegion t
383386 , Monad
387+ , MonadFix
384388 , MonadHold t
389+ , MonadIO
390+ , MonadNodeId
391+ , MonadReflexCreateTrigger t
385392 , MonadSample t
386− , MonadFix
387− , TriggerEvent t
388− , PerformEvent t
389393 , NotReady t
390− , MonadReflexCreateTrigger t
391− , HasDisplaySize t
394+ , PerformEvent t
392395 , PostBuild t
393− , MonadNodeId
394− , MonadIO
396+ , TriggerEvent t
395397 )
396398
397399instance MonadTrans (Layout t) where
@@ -406,20 +408,10 @@ instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (Layout t m
406408 traverseDMapWithKeyWithAdjust f m e = Layout $ traverseDMapWithKeyWithAdjust (\k v -> unLayout $ f k v) m e
407409 traverseDMapWithKeyWithAdjustWithMove f m e = Layout $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unLayout $ f k v) m e
408410
409−instance (HasVtyWidgetCtx t m, HasDisplaySize t m, Reflex t, MonadFix m) => HasVtyWidgetCtx t (Layout t m) where
410− localCtx f x = do
411− solution <- Layout ask
412− let orientation = snd . rootLT <$> solution
413− lift $ localCtx f $ do
414− dw <- displayWidth
415− dh <- displayHeight
416− let reg = Region 0 0 <$> dw <*> dh
417− runLayout orientation reg x
418−
419411-- | Apply a transformation to the context of a child 'Layout' action and run
420412-- that action
421413hoistRunLayout
422− :: (HasDisplaySize t m, MonadFix m, Monad n)
414+ :: (HasDisplayRegion t m, MonadFix m, Monad n)
423415 => (m a -> n b)
424416 -> Layout t m a
425417 -> Layout t n b
@@ -432,28 +424,31 @@ hoistRunLayout f x = do
432424 let reg = Region 0 0 <$> dw <*> dh
433425 runLayout orientation reg x
434426
435−instance (HasVtyInput t m, HasDisplaySize t m, MonadFix m, Reflex t) => HasVtyInput t (Layout t m) where
427+instance (HasInput t m, HasDisplayRegion t m, MonadFix m, Reflex t) => HasInput t (Layout t m) where
436428 localInput = hoistRunLayout . localInput
437429
438−instance (HasDisplaySize t m, ImageWriter t m, MonadFix m) => ImageWriter t (Layout t m) where
430+instance (HasDisplayRegion t m, HasImageWriter t m, MonadFix m) => HasImageWriter t (Layout t m) where
439431 mapImages f = hoistRunLayout (mapImages f)
440432
441−instance (HasFocus t m, Monad m) => HasFocus t (Layout t m)
433+instance (HasFocusReader t m, Monad m) => HasFocusReader t (Layout t m)
434+
435+instance (HasTheme t m, Monad m) => HasTheme t (Layout t m)
442436
443−instance (Monad m, MonadNodeId m, Reflex t, MonadFix m) => MonadLayout t (Layout t m) where
437+instance (Monad m, MonadNodeId m, Reflex t, MonadFix m) => HasLayout t (Layout t m) where
444438 axis o c (Layout x) = Layout $ do
445439 nodeId <- getNextNodeId
446− (result, forest) <- lift $ local (\t -> fromMaybe (LayoutTree (Region 0 0 0 0, Orientation_Column) mempty) . lookupLF nodeId . childrenLT <$> t) $ runDynamicWriterT x
440+ let dummyParentLayout = LayoutTree (nilRegion, Orientation_Column) mempty
441+ (result, forest) <- lift $ local (\t -> fromMaybe dummyParentLayout . lookupLF nodeId . childrenLT <$> t) $ runDynamicWriterT x
447442 tellDyn $ singletonLF nodeId <$> (LayoutTree <$> ((,) <$> c <*> o) <*> forest)
448443 pure result
449444 region c = do
450445 nodeId <- lift getNextNodeId
451446 Layout $ tellDyn $ ffor c $ \c' -> singletonLF nodeId $ LayoutTree (c', Orientation_Row) mempty
452447 solutions <- Layout ask
453− pure $ maybe (Region 0 0 0 0) (fst . rootLT) . lookupLF nodeId . childrenLT <$> solutions
448+ pure $ maybe nilRegion (fst . rootLT) . lookupLF nodeId . childrenLT <$> solutions
454449 askOrientation = Layout $ asks $ fmap (snd . rootLT)
455450
456−instance (MonadFix m, MonadFocus t m) => MonadFocus t (Layout t m) where
451+instance (MonadFix m, HasFocus t m) => HasFocus t (Layout t m) where
457452 makeFocus = lift makeFocus
458453 requestFocus = lift . requestFocus
459454 isFocused = lift . isFocused
@@ -478,7 +473,7 @@ runLayout o r (Layout x) = do
478473 return result
479474
480475-- | Initialize and run the layout monad, using all of the available screen space.
481−initLayout :: (HasDisplaySize t m, MonadFix m) => Layout t m a -> m a
476+initLayout :: (HasDisplayRegion t m, MonadFix m) => Layout t m a -> m a
482477initLayout f = do
483478 dw <- displayWidth
484479 dh <- displayHeight
@@ -488,13 +483,13 @@ initLayout f = do
488483-- * The tile "window manager"
489484--
490485-- $tiling
491−-- Generally MonadLayout and MonadFocus are used together to build a user
486+-- Generally HasLayout and HasFocus are used together to build a user
492487-- interface. These functions check the available screen size and initialize
493488-- the layout monad with that information, and also initialize the focus monad.
494489
495490-- | Initialize a 'Layout' and 'Focus' management context, returning the produced 'FocusSet'.
496491initManager
497− :: (HasDisplaySize t m, Reflex t, MonadHold t m, MonadFix m)
492+ :: (HasDisplayRegion t m, Reflex t, MonadHold t m, MonadFix m)
498493 => Layout t (Focus t m) a
499494 -> m (a, Dynamic t FocusSet)
500495initManager =
@@ -502,7 +497,7 @@ initManager =
502497
503498-- | Initialize a 'Layout' and 'Focus' management context.
504499initManager_
505− :: (HasDisplaySize t m, Reflex t, MonadHold t m, MonadFix m)
500+ :: (HasDisplayRegion t m, Reflex t, MonadHold t m, MonadFix m)
506501 => Layout t (Focus t m) a
507502 -> m a
508503initManager_ = fmap fst . initManager
@@ -515,7 +510,7 @@ initManager_ = fmap fst . initManager
515510-- provided constraint. Returns the 'FocusId' allowing for manual focus
516511-- management.
517512tile'
518− :: (MonadNodeId m, MonadFix m, Reflex t, HasVtyWidgetCtx t m, HasVtyInput t m, MonadFocus t m, MonadLayout t m, ImageWriter t m)
513+ :: (MonadFix m, Reflex t, HasInput t m, HasFocus t m, HasLayout t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
519514 => Dynamic t Constraint
520515 -> m a
521516 -> m (FocusId, a)
@@ -534,7 +529,7 @@ tile' c w = do
534529-- | A widget that is focusable and occupies a layout region based on the
535530-- provided constraint.
536531tile
537− :: (MonadNodeId m, MonadFix m, Reflex t, HasVtyWidgetCtx t m, HasVtyInput t m, MonadFocus t m, MonadLayout t m, ImageWriter t m)
532+ :: (MonadFix m, Reflex t, HasInput t m, HasFocus t m, HasLayout t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
538533 => Dynamic t Constraint
539534 -> m a
540535 -> m a
@@ -545,7 +540,7 @@ tile c = fmap snd . tile' c
545540-- | A widget that is not focusable and occupies a layout region based on the
546541-- provided constraint.
547542grout
548− :: (Reflex t, MonadNodeId m, HasVtyWidgetCtx t m, MonadLayout t m, HasVtyInput t m, ImageWriter t m)
543+ :: (Reflex t, HasLayout t m, HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
549544 => Dynamic t Constraint
550545 -> m a
551546 -> m a
src/Reflex/Vty/Widget/Split.hsnew file+92
@@ -0,0 +1,92 @@
1+{-|
2+ Description: Widgets that split the display vertically or horizontally.
3+-}
4+module Reflex.Vty.Widget.Split where
5+
6+import Control.Applicative
7+import Control.Monad.Fix
8+import Graphics.Vty as V
9+import Reflex
10+import Reflex.Vty.Widget
11+import Reflex.Vty.Widget.Input.Mouse
12+
13+-- | A split of the available space into two parts with a draggable separator.
14+-- Starts with half the space allocated to each, and the first pane has focus.
15+-- 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+ => m ()
18+ -> m a
19+ -> m b
20+ -> m (a,b)
21+splitVDrag wS wA wB = do
22+ dh <- displayHeight
23+ dw <- displayWidth
24+ h0 <- sample $ current dh -- TODO
25+ dragE <- drag V.BLeft
26+ let splitter0 = h0 `div` 2
27+ rec splitterCheckpoint <- holdDyn splitter0 $ leftmost [fst <$> ffilter snd dragSplitter, resizeSplitter]
28+ splitterPos <- holdDyn splitter0 $ leftmost [fst <$> dragSplitter, resizeSplitter]
29+ splitterFrac <- holdDyn ((1::Double) / 2) $ ffor (attach (current dh) (fst <$> dragSplitter)) $ \(h, x) ->
30+ fromIntegral x / max 1 (fromIntegral h)
31+ let dragSplitter = fforMaybe (attach (current splitterCheckpoint) dragE) $
32+ \(splitterY, Drag (_, fromY) (_, toY) _ _ end) ->
33+ if splitterY == fromY then Just (toY, end) else Nothing
34+ regA = Region 0 0 <$> dw <*> splitterPos
35+ regS = Region 0 <$> splitterPos <*> dw <*> 1
36+ regB = Region 0 <$> (splitterPos + 1) <*> dw <*> (dh - splitterPos - 1)
37+ resizeSplitter = ffor (attach (current splitterFrac) (updated dh)) $
38+ \(frac, h) -> round (frac * fromIntegral h)
39+ focA <- holdDyn True $ leftmost
40+ [ True <$ mA
41+ , False <$ mB
42+ ]
43+ (mA, rA) <- pane regA focA $ withMouseDown wA
44+ pane regS (pure False) wS
45+ (mB, rB) <- pane regB (not <$> focA) $ withMouseDown wB
46+ return (rA, rB)
47+ where
48+ withMouseDown x = do
49+ m <- mouseDown V.BLeft
50+ x' <- x
51+ return (m, x')
52+
53+-- | A plain split of the available space into vertically stacked panes.
54+-- 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+ => Dynamic t (Int -> Int)
57+ -- ^ Function used to determine size of first pane based on available size
58+ -> Dynamic t (Bool, Bool)
59+ -- ^ How to focus the two sub-panes, given that we are focused.
60+ -> m a
61+ -- ^ Widget for first pane
62+ -> m b
63+ -- ^ Widget for second pane
64+ -> m (a,b)
65+splitV sizeFunD focD wA wB = do
66+ dw <- displayWidth
67+ dh <- displayHeight
68+ let regA = Region 0 0 <$> dw <*> (sizeFunD <*> dh)
69+ regB = Region 0 <$> (_region_height <$> regA) <*> dw <*> liftA2 (-) dh (_region_height <$> regA)
70+ ra <- pane regA (fst <$> focD) wA
71+ rb <- pane regB (snd <$> focD) wB
72+ return (ra,rb)
73+
74+-- | A plain split of the available space into horizontally stacked panes.
75+-- 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+ => Dynamic t (Int -> Int)
78+ -- ^ Function used to determine size of first pane based on available size
79+ -> Dynamic t (Bool, Bool)
80+ -- ^ How to focus the two sub-panes, given that we are focused.
81+ -> m a
82+ -- ^ Widget for first pane
83+ -> m b
84+ -- ^ Widget for second pane
85+ -> m (a,b)
86+splitH sizeFunD focD wA wB = do
87+ dw <- displayWidth
88+ dh <- displayHeight
89+ let regA = Region 0 0 <$> (sizeFunD <*> dw) <*> dh
90+ regB = Region <$> (_region_width <$> regA) <*> 0 <*> liftA2 (-) dw (_region_width <$> regA) <*> dh
91+ liftA2 (,) (pane regA (fmap fst focD) wA) (pane regB (fmap snd focD) wB)
92+
src/Reflex/Vty/Widget/Text.hsnew file+106
@@ -0,0 +1,106 @@
1+{-|
2+ Description: Text- and character-rendering widgets
3+-}
4+module Reflex.Vty.Widget.Text where
5+
6+import Control.Monad.Fix
7+import Data.Default
8+import Data.Text (Text)
9+import qualified Data.Text as T
10+import Data.Text.Zipper as TZ
11+import qualified Graphics.Vty as V
12+import Reflex
13+import Reflex.Vty.Widget
14+import Reflex.Vty.Widget.Input.Mouse
15+
16+-- | Fill the background with a particular character.
17+fill :: (HasDisplayRegion t m, HasImageWriter t m, HasTheme t m) => Behavior t Char -> m ()
18+fill bc = do
19+ dw <- displayWidth
20+ dh <- displayHeight
21+ bt <- theme
22+ let fillImg =
23+ (\attr w h c -> [V.charFill attr c w h])
24+ <$> bt
25+ <*> current dw
26+ <*> current dh
27+ <*> bc
28+ tellImages fillImg
29+
30+-- | Configuration options for displaying "rich" text
31+data RichTextConfig t = RichTextConfig
32+ { _richTextConfig_attributes :: Behavior t V.Attr
33+ }
34+
35+instance Reflex t => Default (RichTextConfig t) where
36+ def = RichTextConfig $ pure V.defAttr
37+
38+
39+-- TODO delete this and use new local theming
40+-- | A widget that displays text with custom time-varying attributes
41+richText
42+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m, HasTheme t m)
43+ => RichTextConfig t
44+ -> Behavior t Text
45+ -> m ()
46+richText cfg t = do
47+ dw <- displayWidth
48+ let img = (\w a s -> [wrapText w a s])
49+ <$> current dw
50+ <*> _richTextConfig_attributes cfg
51+ <*> t
52+ tellImages img
53+ where
54+ wrapText maxWidth attrs = V.vertCat
55+ . concatMap (fmap (V.string attrs . T.unpack) . TZ.wrapWithOffset maxWidth 0)
56+ . T.split (=='\n')
57+
58+-- | Renders text, wrapped to the container width
59+text
60+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m, HasTheme t m)
61+ => Behavior t Text
62+ -> m ()
63+text t = do
64+ bt <- theme
65+ richText (RichTextConfig bt) t
66+
67+-- | Scrollable text widget. The output pair exposes the current scroll position and total number of lines (including those
68+-- that are hidden)
69+scrollableText
70+ :: forall t m. (Reflex t, MonadHold t m, MonadFix m, HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasTheme t m)
71+ => Event t Int
72+ -- ^ Number of lines to scroll by
73+ -> Behavior t Text
74+ -> m (Behavior t (Int, Int))
75+ -- ^ (Current scroll position, total number of lines)
76+scrollableText scrollBy t = do
77+ dw <- displayWidth
78+ bt <- theme
79+ let imgs = wrap <$> bt <*> current dw <*> t
80+ kup <- key V.KUp
81+ kdown <- key V.KDown
82+ m <- mouseScroll
83+ let requestedScroll :: Event t Int
84+ requestedScroll = leftmost
85+ [ 1 <$ kdown
86+ , (-1) <$ kup
87+ , ffor m $ \case
88+ ScrollDirection_Up -> (-1)
89+ ScrollDirection_Down -> 1
90+ , scrollBy
91+ ]
92+ updateLine maxN delta ix = min (max 0 (ix + delta)) maxN
93+ lineIndex :: Dynamic t Int <- foldDyn (\(maxN, delta) ix -> updateLine (maxN - 1) delta ix) 0 $
94+ attach (length <$> imgs) requestedScroll
95+ tellImages $ fmap ((:[]) . V.vertCat) $ drop <$> current lineIndex <*> imgs
96+ return $ (,) <$> ((+) <$> current lineIndex <*> pure 1) <*> (length <$> imgs)
97+ where
98+ wrap attr maxWidth = concatMap (fmap (V.string attr . T.unpack) . TZ.wrapWithOffset maxWidth 0) . T.split (=='\n')
99+
100+-- | Renders any behavior whose value can be converted to
101+-- 'String' as text
102+display
103+ :: (Reflex t, Monad m, Show a, HasDisplayRegion t m, HasImageWriter t m, HasTheme t m)
104+ => Behavior t a
105+ -> m ()
106+display a = text $ T.pack . show <$> a
test/Data/Text/ZipperSpec.hsnew file+104
@@ -0,0 +1,104 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Data.Text.ZipperSpec(
4+ spec
5+) where
6+
7+import Prelude
8+
9+import Test.Hspec
10+
11+import qualified Data.Map as Map
12+import qualified Data.Text as T
13+import Control.Monad
14+
15+import Data.Text.Zipper
16+
17+
18+someSentence = "12345 1234 12"
19+
20+splitSentenceAtDisplayWidth :: Int -> T.Text -> [(T.Text, Bool)]
21+splitSentenceAtDisplayWidth w t = splitWordsAtDisplayWidth w (wordsWithWhitespace t)
22+
23+spec :: Spec
24+spec =
25+ describe "Zipper" $ do
26+ it "wordsWithWhitespace" $ do
27+ wordsWithWhitespace "" `shouldBe` []
28+ wordsWithWhitespace "😱😱😱" `shouldBe` ["😱😱😱"]
29+ wordsWithWhitespace "abcd efgf f" `shouldBe` ["abcd ","efgf ", "f"]
30+ wordsWithWhitespace "aoeu " `shouldBe` ["aoeu "]
31+ it "splitWordsAtDisplayWidth" $ do
32+ fmap fst (splitSentenceAtDisplayWidth 5 "123456") `shouldBe` ["12345","6"]
33+ fmap fst (splitSentenceAtDisplayWidth 5 "12345 6") `shouldBe` ["12345","6"]
34+ fmap fst (splitSentenceAtDisplayWidth 5 "1234 56") `shouldBe` ["1234","56"]
35+ fmap fst (splitSentenceAtDisplayWidth 5 "12345678912345") `shouldBe` ["12345","67891","2345"]
36+ fmap fst (splitSentenceAtDisplayWidth 5 "1234 56") `shouldBe` ["1234 "," 56"]
37+ fmap fst (splitSentenceAtDisplayWidth 8 "1 2 3 4 5 6 7 8 9 1") `shouldBe` ["1 2 3 4","5 6 7 8", "9 1"]
38+ it "wrapWithOffsetAndAlignment" $ do
39+ wrapWithOffsetAndAlignment TextAlignment_Left 5 0 someSentence `shouldBe` [(WrappedLine "12345" True 0), (WrappedLine "1234" True 0), (WrappedLine "12" False 0)]
40+ wrapWithOffsetAndAlignment TextAlignment_Right 5 0 someSentence `shouldBe` [(WrappedLine "12345" True 0), (WrappedLine "1234" True 1), (WrappedLine "12" False 3)]
41+ wrapWithOffsetAndAlignment TextAlignment_Center 5 0 someSentence `shouldBe` [(WrappedLine "12345" True 0), (WrappedLine "1234" True 0), (WrappedLine "12" False 1)]
42+ wrapWithOffsetAndAlignment TextAlignment_Left 5 1 someSentence `shouldBe` [(WrappedLine "1234" False 0), (WrappedLine "5" True 0), (WrappedLine "1234" True 0), (WrappedLine "12" False 0)]
43+
44+ -- leading spaces and offset case
45+ wrapWithOffsetAndAlignment TextAlignment_Left 5 1 (" " <> someSentence) `shouldBe` [(WrappedLine " " True 0), (WrappedLine "12345" True 0), (WrappedLine "1234" True 0), (WrappedLine "12" False 0)]
46+ it "eolSpacesToLogicalLines" $ do
47+ eolSpacesToLogicalLines
48+ [
49+ [ (WrappedLine "😱" True 1), (WrappedLine "😱" False 2), (WrappedLine "😱" False 3) ]
50+ , [ (WrappedLine "aa" True 1), (WrappedLine "aa" True 2), (WrappedLine "aa" False 3) ]
51+ ]
52+ `shouldBe`
53+ [
54+ [ ("😱",1) ]
55+ , [ ("😱",2), ("😱",3) ]
56+ , [ ("aa",1) ]
57+ , [ ("aa",2) ]
58+ , [ ("aa",3) ]
59+ ]
60+ it "offsetMapWithAlignmentInternal" $ do
61+ offsetMapWithAlignmentInternal
62+ [
63+ [ (WrappedLine "😱" True 1), (WrappedLine "😱" False 2), (WrappedLine "😱" False 3) ]
64+ , [ (WrappedLine "aa" True 1), (WrappedLine "aa" True 2), (WrappedLine "aa" False 3) ]
65+ ]
66+ `shouldBe`
67+ Map.fromList
68+ [ (0, (1,0))
69+ , (1, (2,2)) -- jump by 1 for char and 1 for space
70+ , (2, (3,3)) -- jump by 1 for char
71+ , (3, (1,5)) -- jump by 1 for char and 1 for newline
72+ , (4, (2,8)) -- jump by 2 for char and 1 for space
73+ , (5, (3,11)) -- jump by 2 for char and 1 for space
74+ ]
75+ it "displayLines - cursorPos" $ do
76+ let
77+ dl0 = displayLinesWithAlignment TextAlignment_Right 10 () () (fromText "")
78+ dl1 = displayLinesWithAlignment TextAlignment_Right 10 () () (fromText "aoeu")
79+ dl2 = displayLinesWithAlignment TextAlignment_Right 10 () () (fromText "aoeu\n")
80+ dl3 = displayLinesWithAlignment TextAlignment_Right 10 () () (fromText "0123456789")
81+ dl4 = displayLinesWithAlignment TextAlignment_Right 10 () () (insertChar 'a' $ fromText "aoeu")
82+ dl5 = displayLinesWithAlignment TextAlignment_Right 10 () () (left $ insertChar 'a' $ fromText "aoeu")
83+ dl6 = displayLinesWithAlignment TextAlignment_Right 10 () () (deleteLeft $ insertChar 'a' $ fromText "aoeu")
84+ _displayLines_cursorPos dl0 `shouldBe` (0,0)
85+ _displayLines_cursorPos dl1 `shouldBe` (4,0)
86+ _displayLines_cursorPos dl2 `shouldBe` (0,1)
87+ _displayLines_cursorPos dl3 `shouldBe` (0,1)
88+ _displayLines_cursorPos dl4 `shouldBe` (5,0)
89+ _displayLines_cursorPos dl5 `shouldBe` (4,0)
90+ _displayLines_cursorPos dl6 `shouldBe` (4,0)
91+ it "displayLines - offsetMap" $ do
92+ let
93+ dl0 = displayLinesWithAlignment TextAlignment_Left 5 () () (end $ fromText "aoeku")
94+ _displayLines_cursorPos dl0 `shouldBe` (0,1)
95+ Map.size (_displayLines_offsetMap dl0) `shouldBe` 2 -- cursor character is on second line
96+ it "displayLinesWithAlignment - spans" $ do
97+ let
98+ someText = top $ fromText "0123456789abcdefgh"
99+ -- outer span length should be invariant when changing TextAlignment and CursorPosition
100+ forM_ [0..4] $ \x -> do
101+ forM_ [TextAlignment_Left, TextAlignment_Center, TextAlignment_Right] $ \ta -> do
102+ let t = rightN x $ someText
103+ length (_displayLines_spans $ (displayLinesWithAlignment ta 5 () () t)) `shouldBe` 4
104+ length (_displayLines_spans $ (displayLinesWithAlignment ta 10 () () t)) `shouldBe` 2
test/Spec.hsnew file+15
@@ -0,0 +1,15 @@
1+-- hspec auto-discovery stuff
2+-- does not work with current CircleCI image so instead we do it manually for now
3+-- {-# OPTIONS_GHC -F -pgmF hspec-discover #-}
4+
5+
6+import Test.Hspec
7+
8+import qualified Data.Text.ZipperSpec
9+
10+main :: IO ()
11+main = hspec spec
12+
13+spec :: Spec
14+spec = do
15+ describe "Data.Text.ZipperSpec" Data.Text.ZipperSpec.spec