on develop help sign in
.github/workflows/haskell.yml+12−10
since splitsnapshots
unifiedsplit
@@ -1,33 +1,33 @@
1−name: Haskell CI
1+name: github-action
2
2
3on: [push, pull_request]
3on: [push, pull_request]
4
4
5jobs:
5jobs:
6 build:
6 build:
7−
8− runs-on: ${{ matrix.os }}
9 strategy:
7 strategy:
10 matrix:
8 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']
12 os: ['ubuntu-latest', 'macos-latest']
10 os: ['ubuntu-latest', 'macos-latest']
11+ runs-on: ${{ matrix.os }}
12+
13+ name: GHC ${{ matrix.ghc }} on ${{ matrix.os }}
13 steps:
14 steps:
14 - uses: actions/checkout@v2
15 - uses: actions/checkout@v2
15 - uses: actions/setup-haskell@v1
16 - uses: actions/setup-haskell@v1
16 with:
17 with:
17 ghc-version: ${{ matrix.ghc }}
18 ghc-version: ${{ matrix.ghc }}
18− cabal-version: '3.0'
19−
20 - name: Cache
19 - name: Cache
21 uses: actions/cache@v1
20 uses: actions/cache@v1
22 env:
21 env:
23 cache-name: cache-cabal
22 cache-name: cache-cabal
24 with:
23 with:
25 path: ~/.cabal
24 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') }}
27 restore-keys: |
26 restore-keys: |
28− ${{ runner.os }}-build-${{ env.cache-name }}-
27+ ${{ runner.os }}-${{ matrix.ghc }}-build-${{ env.cache-name }}-
29− ${{ runner.os }}-build-
28+ ${{ runner.os }}-${{ matrix.ghc }}-build-
30− ${{ runner.os }}-
29+ ${{ runner.os }}-${{ matrix.ghc }}-
30+ ${{ runner.os }}
31
31
32 - name: Install dependencies
32 - name: Install dependencies
33 run: |
33 run: |
@@ -37,3 +37,5 @@ jobs:
37 run: cabal build --enable-tests --enable-benchmarks all
37 run: cabal build --enable-tests --enable-benchmarks all
38 - name: Run tests
38 - name: Run tests
39 run: cabal test all
39 run: cabal test all
40+ - name: Build Docs
41+ run: cabal haddock
.travis.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+91−3
@@ -1,8 +1,96 @@
1# Revision history for reflex-vty
1# Revision history for reflex-vty
2
2
3−## Unreleased
3+## 0.2.0.0
4+
5+* _Module Reorganization_: The following modules have been added (and are all re-exported by Reflex.Vty):
6+ * Reflex.Vty.Widget.Box for all the box functions and datatypes
7+ * Reflex.Vty.Widget.Input.Mouse for clicking, dragging, and scrolling
8+ * Reflex.Vty.Widget.Split contains `splitV`, `splitH`, etc
9+ * Reflex.Vty.Widget.Text contains text rendering functions like `text` and `display`
10+* _Bugfixes_:
4−* Remove text-icu dependency and switch to `wcwidth` from vty package to compute character width in `Data.Text.Zipper`.
11+ * Remove text-icu dependency and switch to `wcwidth` from vty package to compute character width in `Data.Text.Zipper`.
5−* `goToDisplayLinePosition` in `Data.Text.Zipper` correctly accounts for character width now.
12+ * `goToDisplayLinePosition` in `Data.Text.Zipper` correctly accounts for character width now.
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
6
94
7## 0.1.4.1
95## 0.1.4.1
8* Migrate to new dependent-sum / dependent-map (after the "some" package split)
96* Migrate to new dependent-sum / dependent-map (after the "some" package split)
reflex-vty.cabal+92−10
@@ -1,5 +1,5 @@
1name: reflex-vty
1name: reflex-vty
2−version: 0.1.4.1
2+version: 0.2.0.0
3synopsis: Reflex FRP host and widgets for VTY applications
3synopsis: Reflex FRP host and widgets for VTY applications
4description:
4description:
5 Build terminal applications using functional reactive programming (FRP) with Reflex FRP (<https://reflex-frp.org>).
5 Build terminal applications using functional reactive programming (FRP) with Reflex FRP (<https://reflex-frp.org>).
@@ -12,27 +12,35 @@ maintainer: maintainer@obsidian.systems
12copyright: 2020 Obsidian Systems LLC
12copyright: 2020 Obsidian Systems LLC
13category: FRP
13category: FRP
14build-type: Simple
14build-type: Simple
15−cabal-version: >=1.18
15+cabal-version: 1.18
16extra-source-files:
16extra-source-files:
17 README.md
17 README.md
18 ChangeLog.md
18 ChangeLog.md
19extra-doc-files: doc/tasks.png
19extra-doc-files: doc/tasks.png
20−tested-with: 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
21
26
22library
27library
23 exposed-modules: Reflex.Vty
28 exposed-modules: Reflex.Vty
24 , Reflex.Vty.Host
29 , Reflex.Vty.Host
25 , Reflex.Vty.Widget
30 , Reflex.Vty.Widget
31+ , Reflex.Vty.Widget.Box
26 , Reflex.Vty.Widget.Input
32 , Reflex.Vty.Widget.Input
33+ , Reflex.Vty.Widget.Input.Mouse
27 , Reflex.Vty.Widget.Input.Text
34 , Reflex.Vty.Widget.Input.Text
28 , Reflex.Vty.Widget.Layout
35 , Reflex.Vty.Widget.Layout
36+ , Reflex.Vty.Widget.Split
37+ , Reflex.Vty.Widget.Text
29 , Data.Text.Zipper
38 , Data.Text.Zipper
30− , Reflex.Class.Switchable
31 , Reflex.Spider.Orphans
39 , Reflex.Spider.Orphans
32 , Control.Monad.NodeId
40 , Control.Monad.NodeId
33 build-depends:
41 build-depends:
34− base >= 4.10.0 && < 4.14,
42+ base >= 4.10.0 && < 4.15,
35− bimap >= 0.3.3 && < 0.4,
43+ bimap >= 0.3.3 && < 0.5,
36 containers >= 0.5.0 && < 0.7,
44 containers >= 0.5.0 && < 0.7,
37 mtl >= 2.2.2 && < 2.3,
45 mtl >= 2.2.2 && < 2.3,
38 transformers >= 0.5.5 && < 0.6,
46 transformers >= 0.5.5 && < 0.6,
@@ -42,6 +50,7 @@ library
42 text >= 1.2.3 && < 1.3,
50 text >= 1.2.3 && < 1.3,
43 dependent-sum >= 0.7 && < 0.8,
51 dependent-sum >= 0.7 && < 0.8,
44 exception-transformers >= 0.4.0 && < 0.5,
52 exception-transformers >= 0.4.0 && < 0.5,
53+ mmorph >= 1.1 && < 1.2,
45 ordered-containers >= 0.2.2 && < 0.3,
54 ordered-containers >= 0.2.2 && < 0.3,
46 primitive >= 0.6.3 && < 0.8,
55 primitive >= 0.6.3 && < 0.8,
47 ref-tf >= 0.4.0 && < 0.5,
56 ref-tf >= 0.4.0 && < 0.5,
@@ -51,11 +60,40 @@ library
51 hs-source-dirs: src
60 hs-source-dirs: src
52 default-language: Haskell2010
61 default-language: Haskell2010
53 ghc-options: -Wall
62 ghc-options: -Wall
63+ default-extensions:
64+ BangPatterns
65+ ConstraintKinds
66+ CPP
67+ DataKinds
68+ DefaultSignatures
69+ DeriveFunctor
70+ DeriveGeneric
71+ FlexibleContexts
72+ FlexibleInstances
73+ FunctionalDependencies
74+ GADTs
75+ GeneralizedNewtypeDeriving
76+ KindSignatures
77+ LambdaCase
78+ MultiParamTypeClasses
79+ MultiWayIf
80+ OverloadedStrings
81+ PatternGuards
82+ PolyKinds
83+ QuasiQuotes
84+ RankNTypes
85+ RecursiveDo
86+ ScopedTypeVariables
87+ StandaloneDeriving
88+ TemplateHaskell
89+ TupleSections
90+ TypeApplications
91+ TypeFamilies
54
92
55executable example
93executable example
56 hs-source-dirs: src-bin
94 hs-source-dirs: src-bin
57 main-is: example.hs
95 main-is: example.hs
58− ghc-options: -threaded
96+ ghc-options: -threaded -Wall
59 build-depends:
97 build-depends:
60 base,
98 base,
61 containers,
99 containers,
@@ -66,7 +104,51 @@ executable example
66 transformers,
104 transformers,
67 vty
105 vty
68 default-language: Haskell2010
106 default-language: Haskell2010
107+ other-modules: Example.CPU
108+ default-extensions:
109+ BangPatterns
110+ ConstraintKinds
111+ CPP
112+ DataKinds
113+ DefaultSignatures
114+ DeriveFunctor
115+ DeriveGeneric
116+ FlexibleContexts
117+ FlexibleInstances
118+ FunctionalDependencies
119+ GADTs
120+ GeneralizedNewtypeDeriving
121+ KindSignatures
122+ LambdaCase
123+ MultiParamTypeClasses
124+ MultiWayIf
125+ OverloadedStrings
126+ PatternGuards
127+ PolyKinds
128+ QuasiQuotes
129+ RankNTypes
130+ RecursiveDo
131+ ScopedTypeVariables
132+ StandaloneDeriving
133+ TemplateHaskell
134+ TupleSections
135+ TypeApplications
136+ TypeFamilies
69
137
70−source-repository head
138+
71− type: git
139+test-suite reflex-vty-test
72− location: https://github.com/reflex-frp/reflex-vty
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+213−151
@@ -1,130 +1,151 @@
1−{-# LANGUAGE FlexibleContexts #-}
2−{-# LANGUAGE FlexibleInstances #-}
3−{-# LANGUAGE GADTs #-}
4−{-# LANGUAGE LambdaCase #-}
5−{-# LANGUAGE MultiParamTypeClasses #-}
6−{-# LANGUAGE OverloadedStrings #-}
7−{-# LANGUAGE RankNTypes #-}
8−{-# LANGUAGE RecursiveDo #-}
9−{-# LANGUAGE ScopedTypeVariables #-}
10−{-# OPTIONS_GHC -threaded #-}
11−
12import Control.Applicative
1import Control.Applicative
13import Control.Monad
2import Control.Monad
14import Control.Monad.Fix
3import Control.Monad.Fix
15−import Control.Monad.NodeId
4+import Data.Functor
16import Data.Functor.Misc
5import Data.Functor.Misc
17import Data.Map (Map)
6import Data.Map (Map)
18−import Data.Maybe
19import qualified Data.Map as Map
7import qualified Data.Map as Map
8+import qualified Data.Set as Set
20import Data.Text (Text)
9import Data.Text (Text)
21import qualified Data.Text as T
10import qualified Data.Text as T
22import qualified Data.Text.Zipper as TZ
11import qualified Data.Text.Zipper as TZ
23import qualified Graphics.Vty as V
12import qualified Graphics.Vty as V
24import Reflex
13import Reflex
25import Reflex.Network
14import Reflex.Network
26−import Reflex.Class.Switchable
27import Reflex.Vty
15import Reflex.Vty
28
16
17+import Example.CPU
18+
19+type VtyExample t m =
20+ ( MonadFix m
21+ , Reflex t
22+ , HasInput t m
23+ , HasImageWriter t m
24+ , HasDisplayRegion t m
25+ , HasFocus t m
26+ , HasFocusReader t m, HasTheme t m
27+ )
28+
29+type Manager t m =
30+ ( HasLayout t m
31+ , HasFocus t m
32+ )
33+
29data Example = Example_TextEditor
34data Example = Example_TextEditor
30 | Example_Todo
35 | Example_Todo
31 | Example_ScrollableTextDisplay
36 | Example_ScrollableTextDisplay
37+ | Example_ClickButtonsGetEmojis
38+ | Example_CPUStat
32 deriving (Show, Read, Eq, Ord, Enum, Bounded)
39 deriving (Show, Read, Eq, Ord, Enum, Bounded)
33
40
41+withCtrlC :: (Monad m, HasInput t m, Reflex t) => m () -> m (Event t ())
42+withCtrlC f = do
43+ 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+
34main :: IO ()
57main :: IO ()
35−main = mainWidget $ do
58+main = mainWidget $ withCtrlC $ do
59+ initManager_ $ do
60+ tabNavigation
61+ let gf = grout . fixed
62+ t = tile flex
63+ buttons = col $ do
64+ gf 3 $ col $ do
65+ gf 1 $ text "Select an example."
66+ gf 1 $ text "Esc will bring you back here."
67+ gf 1 $ text "Ctrl+c to quit."
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"
73+ return $ leftmost
74+ [ Left Example_Todo <$ a
75+ , Left Example_TextEditor <$ b
76+ , Left Example_ScrollableTextDisplay <$ c
77+ , Left Example_ClickButtonsGetEmojis <$ d
78+ , Left Example_CPUStat <$ e
79+ ]
80+ let escapable w = do
81+ void w
82+ i <- input
83+ return $ fforMaybe i $ \case
84+ V.EvKey V.KEsc [] -> Just $ Right ()
85+ _ -> Nothing
86+ rec out <- networkHold buttons $ ffor (switch (current out)) $ \case
87+ Left Example_Todo -> escapable taskList
88+ Left Example_TextEditor -> escapable $ localTheme (const (constant darkTheme)) testBoxes
89+ Left Example_ScrollableTextDisplay -> escapable scrolling
90+ Left Example_ClickButtonsGetEmojis -> escapable easyExample
91+ Left Example_CPUStat -> escapable cpuStats
92+ Right () -> buttons
93+ return ()
94+
95+-- * Mouse button and emojis example
96+easyExample :: (VtyExample t m, Manager t m, MonadHold t m) => m (Event t ())
97+easyExample = do
98+ row $ grout (fixed 39) $ col $ do
99+ (a1,b1,c1) <- grout (fixed 3) $ row $ do
100+ a <- tile flex $ btn "POTATO"
101+ b <- tile flex $ btn "TOMATO"
102+ c <- tile flex $ btn "EGGPLANT"
103+ return (a,b,c)
104+ (a2,b2,c2) <- grout (fixed 3) $ row $ do
105+ a <- tile flex $ btn "CHEESE"
106+ b <- tile flex $ btn "BEES"
107+ c <- tile flex $ btn "MY KNEES"
108+ return (a,b,c)
109+ (a3,b3,c3) <- grout (fixed 3) $ row $ do
110+ a <- tile flex $ btn "TIME"
111+ b <- tile flex $ btn "RHYME"
112+ c <- tile flex $ btn "A BIG CRIME"
113+ return (a,b,c)
114+ tile (fixed 7) $ boxTitle (constant def) "CLICK BUTTONS TO DRAW*" $ do
115+ outputDyn <- foldDyn (<>) "" $ mergeWith (<>)
116+ [a1 $> "\129364", b1 $> "🍅", c1 $> "🍆", a2 $> "\129472", b2 $> "🐝🐝", c2 $> "💘", a3 $> "⏰", b3 $> "📜", c3 $> "💰🔪🔒"]
117+ text (current outputDyn)
118+ tile flex $ text "* Requires font support for emojis. Box may render incorrectly unless your vty is initialized with an updated char width map."
36 inp <- input
119 inp <- input
37− let buttons = col $ do
38− fixed 4 $ col $ do
39− fixed 1 $ text "Select an example."
40− fixed 1 $ text "Esc will bring you back here."
41− fixed 1 $ text "Ctrl+c to quit."
42− a <- fixed 5 $ textButtonStatic def "Todo List"
43− b <- fixed 5 $ textButtonStatic def "Text Editor"
44− c <- fixed 5 $ textButtonStatic def "Scrollable text display"
45− return $ leftmost
46− [ Left Example_Todo <$ a
47− , Left Example_TextEditor <$ b
48− , Left Example_ScrollableTextDisplay <$ c
49− ]
50− escapable w = do
51− void w
52− i <- input
53− return $ fforMaybe i $ \case
54− V.EvKey V.KEsc [] -> Just $ Right ()
55− _ -> Nothing
56− rec out <- networkHold buttons $ ffor (switch (current out)) $ \case
57− Left Example_TextEditor -> escapable testBoxes
58− Left Example_Todo -> escapable taskList
59− Left Example_ScrollableTextDisplay -> escapable scrolling
60− Right () -> buttons
61 return $ fforMaybe inp $ \case
120 return $ fforMaybe inp $ \case
62 V.EvKey (V.KChar 'c') [V.MCtrl] -> Just ()
121 V.EvKey (V.KChar 'c') [V.MCtrl] -> Just ()
63 _ -> Nothing
122 _ -> Nothing
64−
123+ where
124+ btn label = do
125+ let cfg = def { _buttonConfig_focusStyle = pure doubleBoxStyle }
126+ buttonClick <- textButtonStatic cfg label
127+ keyPress <- keyCombos $ Set.fromList
128+ [ (V.KEnter, [])
129+ , (V.KChar ' ', [])
130+ ]
131+ pure $ leftmost [() <$ buttonClick, () <$ keyPress]
132+
133+-- * Task list example
65taskList
134taskList
66− :: (Reflex t, MonadHold t m, MonadFix m, Adjustable t m, NotReady t m, PostBuild t m, MonadNodeId m)
135+ :: (VtyExample t m, Manager t m, MonadHold t m, Adjustable t m, PostBuild t m)
67− => VtyWidget t m ()
136+ => m ()
68−taskList = do
137+taskList = col $ do
69− let btn = textButtonStatic def "Add another task"
70− inp <- input
71 let todos0 =
138 let todos0 =
72 [ Todo "Find reflex-vty" True
139 [ Todo "Find reflex-vty" True
73 , Todo "Become functional reactive" False
140 , Todo "Become functional reactive" False
74 , Todo "Make vty apps" False
141 , Todo "Make vty apps" False
75 ]
142 ]
76− rec let todos' = todos todos0 $ leftmost
143+ btn = textButtonStatic def "Add another task"
77− [ () <$ e
144+ enter <- fmap (const ()) <$> key V.KEnter
78− , fforMaybe inp $ \case
145+ rec void $ grout flex $ todos todos0 $ enter <> click
79− V.EvKey V.KEnter [] -> Just ()
146+ click <- tile (fixed 3) btn
80− _ -> Nothing
81− ]
82− (m, (e, _)) <- splitV (pure (subtract 6)) (pure (True, True)) todos' $
83− splitV (pure (subtract 3)) (pure (True, True)) btn (display $ Map.size <$> current m)
84 return ()
147 return ()
85
148
86−testBoxes
87− :: (Reflex t, MonadHold t m, MonadFix m, MonadNodeId m)
88− => VtyWidget t m ()
89−testBoxes = do
90− dw <- displayWidth
91− dh <- displayHeight
92− let region1 = DynRegion (div' dw 6) (div' dh 6) (div' dw 2) (div' dh 2)
93− region2 = DynRegion (div' dw 4) (div' dh 4) (2 * div' dw 3) (2 * div' dh 3)
94− pane region1 (constDyn False) . boxStatic singleBoxStyle $ debugInput
95− _ <- pane region2 (constDyn True) . boxStatic singleBoxStyle $
96− let cfg = def
97− { _textInputConfig_initialValue =
98− "This box is a text input. The box below responds to mouse drag inputs. You can also drag the separator between the boxes to resize them."
99− }
100− textBox = boxTitle (pure roundedBoxStyle) "Text Edit" $
101− multilineTextInput cfg
102− dragBox = boxStatic roundedBoxStyle dragTest
103− in splitVDrag (hRule doubleBoxStyle) textBox dragBox
104− return ()
105− where
106− div' :: (Integral a, Applicative f) => f a -> f a -> f a
107− div' = liftA2 div
108−
109−debugFocus :: (Reflex t, Monad m) => VtyWidget t m ()
110−debugFocus = do
111− f <- focus
112− text $ T.pack . show <$> current f
113−
114−debugInput :: (Reflex t, MonadHold t m) => VtyWidget t m ()
115−debugInput = do
116− lastEvent <- hold "No event yet" . fmap show =<< input
117− text $ T.pack <$> lastEvent
118−
119−dragTest :: (Reflex t, MonadHold t m, MonadFix m) => VtyWidget t m ()
120−dragTest = do
121− lastEvent <- hold "No event yet" . fmap show =<< drag V.BLeft
122− text $ T.pack <$> lastEvent
123−
124−testStringBox :: (Reflex t, Monad m, MonadNodeId m) => VtyWidget t m ()
125−testStringBox = boxStatic singleBoxStyle .
126− text . pure . T.pack . take 500 $ cycle ('\n' : ['a'..'z'])
127−
128data Todo = Todo
149data Todo = Todo
129 { _todo_label :: Text
150 { _todo_label :: Text
130 , _todo_done :: Bool
151 , _todo_done :: Bool
@@ -135,87 +156,128 @@ data TodoOutput t = TodoOutput
135 { _todoOutput_todo :: Dynamic t Todo
156 { _todoOutput_todo :: Dynamic t Todo
136 , _todoOutput_delete :: Event t ()
157 , _todoOutput_delete :: Event t ()
137 , _todoOutput_height :: Dynamic t Int
158 , _todoOutput_height :: Dynamic t Int
159+ , _todoOutput_focusId :: FocusId
138 }
160 }
139
161
140−instance Reflex t => Switchable t (TodoOutput t) where
141− switching t0 e = TodoOutput
142− <$> switching (_todoOutput_todo t0) (_todoOutput_todo <$> e)
143− <*> switching (_todoOutput_delete t0) (_todoOutput_delete <$> e)
144− <*> switching (_todoOutput_height t0) (_todoOutput_height <$> e)
145−
146todo
162todo
147− :: (MonadHold t m, MonadFix m, Reflex t, MonadNodeId m)
163+ :: (VtyExample t m, Manager t m, MonadHold t m)
148 => Todo
164 => Todo
149− -> VtyWidget t m (TodoOutput t)
165+ -> m (TodoOutput t)
150−todo t0 = do
166+todo t0 = row $ do
151− w <- displayWidth
167+ let toggleKeys = Set.fromList
152− rec let checkboxWidth = 3
168+ [ (V.KChar ' ', [V.MCtrl])
153− checkboxRegion = DynRegion 0 0 checkboxWidth 1
169+ , (V.KChar '@', [V.MCtrl])
154− labelHeight = _textInput_lines ti
170+ ]
155− labelWidth = w - 1 - checkboxWidth
171+ anyChildFocused $ \focused -> do
156− labelLeft = checkboxWidth + 1
172+ toggleE <- keyCombos toggleKeys
157− labelTop = constDyn 0
173+ filterKeys (flip Set.notMember $ Set.insert (V.KChar '\t', []) toggleKeys) $ do
158− labelRegion = DynRegion labelLeft labelTop labelWidth labelHeight
174+ rec let cfg = def
175+ { _checkboxConfig_setValue = setVal
176+ }
159− value <- pane checkboxRegion (pure True) $ checkbox def $ _todo_done t0
177+ value <- tile (fixed 4) $ checkbox cfg $ _todo_done t0
160− (ti, d) <- pane labelRegion (pure True) $ do
178+ let setVal = attachWith (\v _ -> not v) (current value) $ gate (current focused) toggleE
179+ (fid, (ti, d)) <- tile' flex $ do
161− i <- input
180+ i <- input
181+ v <- textInput $ def
162− v <- textInput $ def { _textInputConfig_initialValue = TZ.fromText $ _todo_label t0 }
182+ { _textInputConfig_initialValue = TZ.fromText $ _todo_label t0 }
163− let deleteSelf = attachWithMaybe backspaceOnEmpty (current $ _textInput_value v) i
183+ let deleteSelf = attachWithMaybe backspaceOnEmpty (current $ _textInput_value v) i
164− return (v, deleteSelf)
184+ return (v, deleteSelf)
165− return $ TodoOutput
185+ return $ TodoOutput
166− { _todoOutput_todo = Todo <$> _textInput_value ti <*> value
186+ { _todoOutput_todo = Todo <$> _textInput_value ti <*> value
167− , _todoOutput_delete = d
187+ , _todoOutput_delete = d
168− , _todoOutput_height = _textInput_lines ti
188+ , _todoOutput_height = _textInput_lines ti
189+ , _todoOutput_focusId = fid
169− }
190+ }
170 where
191 where
171 backspaceOnEmpty v = \case
192 backspaceOnEmpty v = \case
172 V.EvKey V.KBS _ | T.null v -> Just ()
193 V.EvKey V.KBS _ | T.null v -> Just ()
173 _ -> Nothing
194 _ -> Nothing
174
195
175−scrolling :: (Reflex t, MonadHold t m, MonadFix m, PostBuild t m, MonadNodeId m) => VtyWidget t m ()
176−scrolling = col $ do
177− fixed 2 $ text "Use your mouse wheel or up and down arrows to scroll:"
178− out <- fixed 5 $ boxStatic def $ scrollableText never $ "Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur. Hi omnes lingua, institutis, legibus inter se differunt. Gallos ab Aquitanis Garumna flumen, a Belgis Matrona et Sequana dividit. Horum omnium fortissimi sunt Belgae, propterea quod a cultu atque humanitate provinciae longissime absunt, minimeque ad eos mercatores saepe commeant atque ea quae ad effeminandos animos pertinent important, proximique sunt Germanis, qui trans Rhenum incolunt, quibuscum continenter bellum gerunt. Qua de causa Helvetii quoque reliquos Gallos virtute praecedunt, quod fere cotidianis proeliis cum Germanis contendunt, cum aut suis finibus eos prohibent aut ipsi in eorum finibus bellum gerunt. Eorum una pars, quam Gallos obtinere dictum est, initium capit a flumine Rhodano, continetur Garumna flumine, Oceano, finibus Belgarum, attingit etiam ab Sequanis et Helvetiis flumen Rhenum, vergit ad septentriones. Belgae ab extremis Galliae finibus oriuntur, pertinent ad inferiorem partem fluminis Rheni, spectant in septentrionem et orientem solem. Aquitania a Garumna flumine ad Pyrenaeos montes et eam partem Oceani quae est ad Hispaniam pertinet; spectat inter occasum solis et septentriones.\nApud Helvetios longe nobilissimus fuit et ditissimus Orgetorix. Is M. Messala, [et P.] M. Pisone consulibus regni cupiditate inductus coniurationem nobilitatis fecit et civitati persuasit ut de finibus suis cum omnibus copiis exirent: perfacile esse, cum virtute omnibus praestarent, totius Galliae imperio potiri. Id hoc facilius iis persuasit, quod undique loci natura Helvetii continentur: una ex parte flumine Rheno latissimo atque altissimo, qui agrum Helvetium a Germanis dividit; altera ex parte monte Iura altissimo, qui est inter Sequanos et Helvetios; tertia lacu Lemanno et flumine Rhodano, qui provinciam nostram ab Helvetiis dividit. His rebus fiebat ut et minus late vagarentur et minus facile finitimis bellum inferre possent; qua ex parte homines bellandi cupidi magno dolore adficiebantur. Pro multitudine autem hominum et pro gloria belli atque fortitudinis angustos se fines habere arbitrabantur, qui in longitudinem milia passuum CCXL, in latitudinem CLXXX patebant."
179− fixed 1 $ text $ ffor out $ \(ix, total) -> "Scrolled to line " <> T.pack (show ix) <> " of " <> T.pack (show total)
180−
181todos
196todos
182 :: forall t m.
197 :: forall t m.
183 ( MonadHold t m
198 ( MonadHold t m
184− , MonadFix m
199+ , Manager t m
185− , Reflex t
200+ , VtyExample t m
186 , Adjustable t m
201 , Adjustable t m
187− , NotReady t m
188 , PostBuild t m
202 , PostBuild t m
189− , MonadNodeId m
190 )
203 )
191 => [Todo]
204 => [Todo]
192 -> Event t ()
205 -> Event t ()
193− -> VtyWidget t m (Dynamic t (Map Int (TodoOutput t)))
206+ -> m (Dynamic t (Map Int (TodoOutput t)))
194todos todos0 newTodo = do
207todos todos0 newTodo = do
195 let todosMap0 = Map.fromList $ zip [0..] todos0
208 let todosMap0 = Map.fromList $ zip [0..] todos0
196− rec tabNav <- tabNavigation
197− let insertNav = 1 <$ insert
198− nav = leftmost [tabNav, insertNav]
199− tileCfg = def { _tileConfig_constraint = pure $ Constraint_Fixed 1}
200− listOut <- runLayout (pure Orientation_Column) 0 nav $
201− listHoldWithKey todosMap0 updates $ \k t -> tile tileCfg $ do
209+ rec listOut <- listHoldWithKey todosMap0 updates $ \k t -> grout (fixed 1) $ do
210+ to <- todo t
202− let sel = select selectOnDelete $ Const2 k
211+ let sel = select selectOnDelete $ Const2 k
203− click <- void <$> mouseDown V.BLeft
204− pb <- getPostBuild
212+ pb <- getPostBuild
205− let focusMe = leftmost [ click, sel, pb ]
213+ requestFocus $ Refocus_Id (_todoOutput_focusId to) <$ leftmost [pb, sel]
206− r <- todo t
214+ pure to
207− return (focusMe, r)
208− let delete = ffor todoDelete $ \k -> Map.singleton k Nothing
215+ let delete = flip Map.singleton Nothing <$> todoDelete
216+ todosMap = joinDynThroughMap $ fmap _todoOutput_todo <$> listOut
217+ insert = ffor (tag (current todosMap) newTodo) $ \m -> case Map.lookupMax m of
218+ Nothing -> Map.singleton 0 $ Just $ Todo "" False
219+ Just (k, _) -> Map.singleton (k+1) $ Just $ Todo "" False
209 updates = leftmost [insert, delete]
220 updates = leftmost [insert, delete]
210 todoDelete = switch . current $
221 todoDelete = switch . current $
211 leftmost . Map.elems . Map.mapWithKey (\k -> (k <$) . _todoOutput_delete) <$> listOut
222 leftmost . Map.elems . Map.mapWithKey (\k -> (k <$) . _todoOutput_delete) <$> listOut
212− todosMap = joinDynThroughMap $ fmap _todoOutput_todo <$> listOut
223+
213− insert = ffor (tag (current todosMap) newTodo) $ \m -> case Map.lookupMax m of
214− Nothing -> Map.singleton 0 $ Just $ Todo "" False
215− Just (k, _) -> Map.singleton (k+1) $ Just $ Todo "" False
216 selectOnDelete = fanMap $ (`Map.singleton` ()) <$> attachWithMaybe
224 selectOnDelete = fanMap $ (`Map.singleton` ()) <$> attachWithMaybe
217 (\m k -> let (before, after) = Map.split k m
225 (\m k -> let (before, after) = Map.split k m
218 in fmap fst $ Map.lookupMax before <|> Map.lookupMin after)
226 in fmap fst $ Map.lookupMax before <|> Map.lookupMin after)
219 (current todosMap)
227 (current todosMap)
220 todoDelete
228 todoDelete
221 return listOut
229 return listOut
230+
231+-- * Scrollable text example
232+
233+scrolling :: (VtyExample t m, MonadHold t m, Manager t m, PostBuild t m) => m ()
234+scrolling = col $ do
235+ grout (fixed 2) $ text "Use your mouse wheel or up and down arrows to scroll:"
236+ (fid, out) <- tile' (fixed 5) $ boxStatic def $ scrollableText never $ "Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur. Hi omnes lingua, institutis, legibus inter se differunt. Gallos ab Aquitanis Garumna flumen, a Belgis Matrona et Sequana dividit. Horum omnium fortissimi sunt Belgae, propterea quod a cultu atque humanitate provinciae longissime absunt, minimeque ad eos mercatores saepe commeant atque ea quae ad effeminandos animos pertinent important, proximique sunt Germanis, qui trans Rhenum incolunt, quibuscum continenter bellum gerunt. Qua de causa Helvetii quoque reliquos Gallos virtute praecedunt, quod fere cotidianis proeliis cum Germanis contendunt, cum aut suis finibus eos prohibent aut ipsi in eorum finibus bellum gerunt. Eorum una pars, quam Gallos obtinere dictum est, initium capit a flumine Rhodano, continetur Garumna flumine, Oceano, finibus Belgarum, attingit etiam ab Sequanis et Helvetiis flumen Rhenum, vergit ad septentriones. Belgae ab extremis Galliae finibus oriuntur, pertinent ad inferiorem partem fluminis Rheni, spectant in septentrionem et orientem solem. Aquitania a Garumna flumine ad Pyrenaeos montes et eam partem Oceani quae est ad Hispaniam pertinet; spectat inter occasum solis et septentriones.\nApud Helvetios longe nobilissimus fuit et ditissimus Orgetorix. Is M. Messala, [et P.] M. Pisone consulibus regni cupiditate inductus coniurationem nobilitatis fecit et civitati persuasit ut de finibus suis cum omnibus copiis exirent: perfacile esse, cum virtute omnibus praestarent, totius Galliae imperio potiri. Id hoc facilius iis persuasit, quod undique loci natura Helvetii continentur: una ex parte flumine Rheno latissimo atque altissimo, qui agrum Helvetium a Germanis dividit; altera ex parte monte Iura altissimo, qui est inter Sequanos et Helvetios; tertia lacu Lemanno et flumine Rhodano, qui provinciam nostram ab Helvetiis dividit. His rebus fiebat ut et minus late vagarentur et minus facile finitimis bellum inferre possent; qua ex parte homines bellandi cupidi magno dolore adficiebantur. Pro multitudine autem hominum et pro gloria belli atque fortitudinis angustos se fines habere arbitrabantur, qui in longitudinem milia passuum CCXL, in latitudinem CLXXX patebant."
237+ pb <- getPostBuild
238+ requestFocus $ Refocus_Id fid <$ pb
239+ grout (fixed 1) $ text $ ffor out $ \(ix, total) -> "Scrolled to line " <> T.pack (show ix) <> " of " <> T.pack (show total)
240+
241+-- * Text editor example with resizable boxes
242+
243+testBoxes
244+ :: (MonadHold t m, VtyExample t m)
245+ => m ()
246+testBoxes = do
247+ dw <- displayWidth
248+ dh <- displayHeight
249+ let region1 = Region <$> (div' dw 6) <*> (div' dh 6) <*> (div' dw 2) <*> (div' dh 2)
250+ region2 = Region <$> (div' dw 4) <*> (div' dh 4) <*> (2 * div' dw 3) <*> (2 * div' dh 3)
251+ pane region1 (constDyn False) . boxStatic singleBoxStyle $ debugInput
252+ _ <- pane region2 (constDyn True) . boxStatic singleBoxStyle $
253+ let cfg = def
254+ { _textInputConfig_initialValue =
255+ "This box is a text input. The box below responds to mouse drag inputs. You can also drag the separator between the boxes to resize them."
256+ }
257+ textBox = boxTitle (pure roundedBoxStyle) "Text Edit" $
258+ multilineTextInput cfg
259+ dragBox = boxStatic roundedBoxStyle dragTest
260+ in splitVDrag (hRule doubleBoxStyle) textBox dragBox
261+ return ()
262+ where
263+ div' :: (Integral a, Applicative f) => f a -> f a -> f a
264+ div' = liftA2 div
265+
266+debugFocus :: (VtyExample t m) => m ()
267+debugFocus = do
268+ f <- focus
269+ text $ T.pack . show <$> current f
270+
271+debugInput :: (VtyExample t m, MonadHold t m) => m ()
272+debugInput = do
273+ lastEvent <- hold "No event yet" . fmap show =<< input
274+ text $ T.pack <$> lastEvent
275+
276+dragTest :: (VtyExample t m, MonadHold t m) => m ()
277+dragTest = do
278+ lastEvent <- hold "No event yet" . fmap show =<< drag V.BLeft
279+ text $ T.pack <$> lastEvent
280+
281+testStringBox :: VtyExample t m => m ()
282+testStringBox = boxStatic singleBoxStyle .
283+ text . pure . T.pack . take 500 $ cycle ('\n' : ['a'..'z'])
src/Control/Monad/NodeId.hs+4−7
@@ -2,12 +2,7 @@
2Module: Control.Monad.NodeId
2Module: Control.Monad.NodeId
3Description: Monad providing a supply of unique identifiers
3Description: Monad providing a supply of unique identifiers
4-}
4-}
5−{-# LANGUAGE DefaultSignatures #-}
6−{-# LANGUAGE FlexibleInstances #-}
7−{-# LANGUAGE GADTs #-}
8−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
9−{-# LANGUAGE MultiParamTypeClasses #-}
10−{-# LANGUAGE UndecidableInstances #-}
5+{-# Language UndecidableInstances #-}
11module Control.Monad.NodeId
6module Control.Monad.NodeId
12 ( NodeId
7 ( NodeId
13 , MonadNodeId (..)
8 , MonadNodeId (..)
@@ -15,6 +10,7 @@ module Control.Monad.NodeId
15 , runNodeIdT
10 , runNodeIdT
16 ) where
11 ) where
17
12
13+import Control.Monad.Morph
18import Control.Monad.Reader
14import Control.Monad.Reader
19import Control.Monad.Ref
15import Control.Monad.Ref
20import Data.IORef
16import Data.IORef
@@ -39,10 +35,12 @@ newtype NodeIdT m a = NodeIdT { unNodeIdT :: ReaderT (IORef NodeId) m a }
39 deriving
35 deriving
40 ( Functor
36 ( Functor
41 , Applicative
37 , Applicative
38+ , MFunctor
42 , Monad
39 , Monad
43 , MonadFix
40 , MonadFix
44 , MonadHold t
41 , MonadHold t
45 , MonadIO
42 , MonadIO
43+ , MonadRef
46 , MonadReflexCreateTrigger t
44 , MonadReflexCreateTrigger t
47 , MonadSample t
45 , MonadSample t
48 , MonadTrans
46 , MonadTrans
@@ -50,7 +48,6 @@ newtype NodeIdT m a = NodeIdT { unNodeIdT :: ReaderT (IORef NodeId) m a }
50 , PerformEvent t
48 , PerformEvent t
51 , PostBuild t
49 , PostBuild t
52 , TriggerEvent t
50 , TriggerEvent t
53− , MonadRef
54 )
51 )
55
52
56instance MonadNodeId m => MonadNodeId (ReaderT x m)
53instance MonadNodeId m => MonadNodeId (ReaderT x m)
src/Data/Text/Zipper.hs+280−161
@@ -7,28 +7,28 @@ Description: A zipper for text documents that allows convenient editing and navi
7'TextZipper's can be converted into 'DisplayLines', which describe how the contents of the zipper will be displayed when wrapped to fit within a container of a certain width. It also provides some convenience facilities for converting interactions with the rendered DisplayLines back into manipulations of the underlying TextZipper.
7'TextZipper's can be converted into 'DisplayLines', which describe how the contents of the zipper will be displayed when wrapped to fit within a container of a certain width. It also provides some convenience facilities for converting interactions with the rendered DisplayLines back into manipulations of the underlying TextZipper.
8
8
9-}
9-}
10−{-# LANGUAGE BangPatterns #-}
11−{-# LANGUAGE FlexibleContexts #-}
12−{-# LANGUAGE LambdaCase #-}
13−{-# LANGUAGE OverloadedStrings #-}
14module Data.Text.Zipper where
10module Data.Text.Zipper where
15
11
12+import Prelude
13+
14+import Control.Exception (assert)
15+import Control.Monad.State (evalState, forM, get, put)
16import Data.Char (isSpace)
16import Data.Char (isSpace)
17import Data.Map (Map)
17import Data.Map (Map)
18−import qualified Data.Map as Map
19import Data.Maybe (fromMaybe)
18import Data.Maybe (fromMaybe)
20import Data.String
19import Data.String
21−import Control.Monad.State (evalState, forM, get, put)
22−
23import Data.Text (Text)
20import Data.Text (Text)
24−import qualified Data.Text as T
25import Data.Text.Internal (Text(..), text)
21import Data.Text.Internal (Text(..), text)
26import Data.Text.Internal.Fusion (stream)
22import Data.Text.Internal.Fusion (stream)
27import Data.Text.Internal.Fusion.Types (Stream(..), Step(..))
23import Data.Text.Internal.Fusion.Types (Stream(..), Step(..))
28import Data.Text.Unsafe
24import Data.Text.Unsafe
25+import qualified Data.List as L
26+import qualified Data.Map as Map
27+import qualified Data.Text as T
29
28
30import Graphics.Text.Width (wcwidth)
29import Graphics.Text.Width (wcwidth)
31
30
31+
32-- | A zipper of the logical text input contents (the "document"). The lines
32-- | A zipper of the logical text input contents (the "document"). The lines
33-- before the line containing the cursor are stored in reverse order.
33-- before the line containing the cursor are stored in reverse order.
34-- The cursor is logically between the "before" and "after" text.
34-- The cursor is logically between the "before" and "after" text.
@@ -170,7 +170,7 @@ deleteLeftWord (TextZipper lb b a la) =
170-- | Insert up to n spaces to get to the next logical column that is a multiple of n
170-- | Insert up to n spaces to get to the next logical column that is a multiple of n
171tab :: Int -> TextZipper -> TextZipper
171tab :: Int -> TextZipper -> TextZipper
172tab n z@(TextZipper _ b _ _) =
172tab n z@(TextZipper _ b _ _) =
173− insert (T.replicate (fromEnum $ n - (T.length b `mod` max 1 n)) " ") z
173+ insert (T.replicate (fromEnum $ n - T.length b `mod` max 1 n) " ") z
174
174
175-- | The plain text contents of the zipper
175-- | The plain text contents of the zipper
176value :: TextZipper -> Text
176value :: TextZipper -> Text
@@ -193,105 +193,35 @@ fromText = flip insert empty
193data Span tag = Span tag Text
193data Span tag = Span tag Text
194 deriving (Show)
194 deriving (Show)
195
195
196+-- | Text alignment type
197+data TextAlignment =
198+ TextAlignment_Left
199+ | TextAlignment_Right
200+ | TextAlignment_Center
201+ deriving (Eq, Show)
202+
203+-- A map from the index (row) of display line to (fst,snd)
204+-- fst: leading empty spaces from left (may be negative) to adjust for alignment
205+-- snd: the text offset from the beginning of the document
206+-- to the first character of the display line
207+type OffsetMapWithAlignment = Map Int (Int, Int)
208+
209+-- helper type representing a single visual line that may be part of a wrapped logical line
210+data WrappedLine = WrappedLine
211+ { _wrappedLines_text :: Text
212+ , _wrappedLines_hiddenWhitespace :: Bool -- ^ 'True' if this line ends with a deleted whitespace character
213+ , _wrappedLines_offset :: Int -- ^ offset from beginning of line
214+ }
215+ deriving (Eq, Show)
216+
196-- | Information about the document as it is displayed (i.e., post-wrapping)
217-- | Information about the document as it is displayed (i.e., post-wrapping)
197data DisplayLines tag = DisplayLines
218data DisplayLines tag = DisplayLines
198 { _displayLines_spans :: [[Span tag]]
219 { _displayLines_spans :: [[Span tag]]
199− , _displayLines_offsetMap :: Map Int Int
220+ , _displayLines_offsetMap :: OffsetMapWithAlignment
200− , _displayLines_cursorY :: Int
221+ , _displayLines_cursorPos :: (Int, Int) -- cursor position relative to upper left hand corner
201 }
222 }
202 deriving (Show)
223 deriving (Show)
203
224
204−-- | Given a width and a 'TextZipper', produce a list of display lines
205−-- (i.e., lines of wrapped text) with special attributes applied to
206−-- certain segments (e.g., the cursor). Additionally, produce the current
207−-- y-coordinate of the cursor and a mapping from display line number to text
208−-- offset
209−displayLines
210− :: Int -- ^ Width, used for wrapping
211− -> tag -- ^ Metadata for normal characters
212− -> tag -- ^ Metadata for the cursor
213− -> TextZipper -- ^ The text input contents and cursor state
214− -> DisplayLines tag
215−displayLines width tag cursorTag (TextZipper lb b a la) =
216− let linesBefore :: [[Text]] -- The wrapped lines before the cursor line
217− linesBefore = map (wrapWithOffset width 0) $ reverse lb
218− linesAfter :: [[Text]] -- The wrapped lines after the cursor line
219− linesAfter = map (wrapWithOffset width 0) la
220− offsets :: Map Int Int
221− offsets = offsetMap $ mconcat
222− [ linesBefore
223− , [wrapWithOffset width 0 $ b <> a]
224− , linesAfter
225− ]
226− spansBefore = map ((:[]) . Span tag) $ concat linesBefore
227− spansAfter = map ((:[]) . Span tag) $ concat linesAfter
228− -- Separate the spans before the cursor into
229− -- * spans that are on earlier display lines (though on the same logical line), and
230− -- * spans that are on the same display line
231− (spansCurrentBefore, spansCurLineBefore) = fromMaybe ([], []) $
232− initLast $ map ((:[]) . Span tag) (wrapWithOffset width 0 b)
233− -- Calculate the number of columns on the cursor's display line before the cursor
234− curLineOffset = spansWidth spansCurLineBefore
235− -- Check whether the spans on the current display line are long enough that
236− -- the cursor has to go to the next line
237− cursorAfterEOL = curLineOffset == width
238− cursorCharWidth = case T.uncons a of
239− Nothing -> 1
240− Just (c, _) -> charWidth c
241− -- Separate the span after the cursor into
242− -- * spans that are on the same display line, and
243− -- * spans that are on later display lines (though on the same logical line)
244− (spansCurLineAfter, spansCurrentAfter) = fromMaybe ([], []) $
245− headTail $ case T.uncons a of
246− Nothing -> [[Span cursorTag " "]]
247− Just (c, rest) ->
248− let o = if cursorAfterEOL then cursorCharWidth else curLineOffset + cursorCharWidth
249− cursor = Span cursorTag (T.singleton c)
250− in case map ((:[]) . Span tag) (wrapWithOffset width o rest) of
251− [] -> [[cursor]]
252− (l:ls) -> (cursor : l) : ls
253− in DisplayLines
254− { _displayLines_spans = concat
255− [ spansBefore
256− , spansCurrentBefore
257− , if cursorAfterEOL
258− then [ spansCurLineBefore, spansCurLineAfter ]
259− else [ spansCurLineBefore <> spansCurLineAfter ]
260− , spansCurrentAfter
261− , spansAfter
262− ]
263− , _displayLines_offsetMap = offsets
264− , _displayLines_cursorY = sum
265− [ length spansBefore
266− , length spansCurrentBefore
267− , if cursorAfterEOL then 1 else 0
268− ]
269− }
270− where
271− initLast :: [a] -> Maybe ([a], a)
272− initLast = \case
273− [] -> Nothing
274− (x:xs) -> case initLast xs of
275− Nothing -> Just ([], x)
276− Just (ys, y) -> Just (x:ys, y)
277− headTail :: [a] -> Maybe (a, [a])
278− headTail = \case
279− [] -> Nothing
280− x:xs -> Just (x, xs)
281−
282−-- | Wraps a logical line of text to fit within the given width. The first
283−-- wrapped line is offset by the number of columns provided. Subsequent wrapped
284−-- lines are not.
285−wrapWithOffset
286− :: Int -- ^ Maximum width
287− -> Int -- ^ Offset for first line
288− -> Text -- ^ Text to be wrapped
289− -> [Text]
290−wrapWithOffset maxWidth _ _ | maxWidth <= 0 = []
291−wrapWithOffset maxWidth n xs =
292− let (firstLine, rest) = splitAtWidth (maxWidth - n) xs
293− in firstLine : (fmap (takeWidth maxWidth) . takeWhile (not . T.null) . iterate (dropWidth maxWidth) $ rest)
294−
295-- | Split a 'Text' at the given column index. For example
225-- | Split a 'Text' at the given column index. For example
296--
226--
297-- > splitAtWidth 3 "ᄀabc" == ("ᄀa", "bc")
227-- > splitAtWidth 3 "ᄀabc" == ("ᄀa", "bc")
@@ -301,16 +231,16 @@ splitAtWidth :: Int -> Text -> (Text, Text)
301splitAtWidth n t@(Text arr off len)
231splitAtWidth n t@(Text arr off len)
302 | n <= 0 = (T.empty, t)
232 | n <= 0 = (T.empty, t)
303 | n >= textWidth t = (t, T.empty)
233 | n >= textWidth t = (t, T.empty)
304− | otherwise = let k = iterNWidth n t
234+ | otherwise = let k = toLogicalIndex n t
305 in (text arr off k, text arr (off+k) (len-k))
235 in (text arr off k, text arr (off+k) (len-k))
306− where
236+
307− iterNWidth :: Int -> Text -> Int
237+toLogicalIndex :: Int -> Text -> Int
308− iterNWidth n' t'@(Text _ _ len') = loop 0 0
238+toLogicalIndex n' t'@(Text _ _ len') = loop 0 0
309− where loop !i !cnt
239+ where loop !i !cnt
310− | i >= len' || cnt + w > n' = i
240+ | i >= len' || cnt + w > n' = i
311− | otherwise = loop (i+d) (cnt + w)
241+ | otherwise = loop (i+d) (cnt + w)
312− where Iter c d = iter t' i
242+ where Iter c d = iter t' i
313− w = charWidth c
243+ w = charWidth c
314
244
315-- | Takes the given number of columns of characters. For example
245-- | Takes the given number of columns of characters. For example
316--
246--
@@ -339,56 +269,6 @@ dropWidth n = snd . splitAtWidth n
339charWidth :: Char -> Int
269charWidth :: Char -> Int
340charWidth = wcwidth
270charWidth = wcwidth
341
271
342−
343−-- | For a given set of wrapped logical lines, computes a map
344−-- from display line index to text offset in the original text.
345−-- This is used to help determine how interactions with the displayed
346−-- text map back to the original text.
347−-- For example, given the document @\"AA\\nBBB\\nCCCCCCCC\\n\"@ wrapped to 5 columns,
348−-- this function will compute the offset in the original document of each character
349−-- in column 1:
350−--
351−-- > AA... (0, 0)
352−-- > BBB.. (1, 3)
353−-- > CCCCC (2, 7) -- (this line wraps to the next row)
354−-- > CCC.. (3, 12)
355−-- > ..... (4, 16)
356−offsetMap
357− :: [[Text]] -- ^ The outer list represents logical lines, and the
358− -- inner list represents the display lines into which
359− -- the logical line has been wrapped
360− -> Map Int Int -- ^ A map from the index (row) of display line to
361− -- the text offset from the beginning of the document
362− -- to the first character of the display line
363−offsetMap ts = evalState (offsetMap' ts) (0, 0)
364− where
365− offsetMap' xs = fmap Map.unions $ forM xs $ \x -> do
366− maps <- forM x $ \line -> do
367− let l = T.length line
368− (dl, o) <- get
369− put (dl + 1, o + l)
370− return $ Map.singleton dl o
371− (dl, o) <- get
372− put (dl, o + 1)
373− return $ Map.insert dl (o + 1) $ Map.unions maps
374−
375−-- | Move the cursor of the given 'TextZipper' to the logical position indicated
376−-- by the given display line coordinates, using the provided 'DisplayLines'
377−-- information. If the x coordinate is beyond the end of a line, the cursor is
378−-- moved to the end of the line.
379−goToDisplayLinePosition :: Int -> Int -> DisplayLines tag -> TextZipper -> TextZipper
380−goToDisplayLinePosition x y dl tz =
381− let offset = Map.lookup y $ _displayLines_offsetMap dl
382− in case offset of
383− Nothing -> tz
384− Just o ->
385− let
386− moveRight = case drop y $ _displayLines_spans dl of
387− [] -> x
388− (s:_) -> charIndexAt x . stream . mconcat . fmap (\(Span _ t) -> t) $ s
389− in rightN (o + min moveRight x) $ top tz
390−
391−
392-- | Get the width of the text in a set of 'Span's, taking into account unicode character widths
272-- | Get the width of the text in a set of 'Span's, taking into account unicode character widths
393spansWidth :: [Span tag] -> Int
273spansWidth :: [Span tag] -> Int
394spansWidth = sum . map (\(Span _ t) -> textWidth t)
274spansWidth = sum . map (\(Span _ t) -> textWidth t)
@@ -424,3 +304,242 @@ charIndexAt pos (Stream next s0 _len) = loop_length 0 0 s0
424 Yield c s' -> if w > pos then i else loop_length (i+1) w s' where
304 Yield c s' -> if w > pos then i else loop_length (i+1) w s' where
425 w = z + charWidth c
305 w = z + charWidth c
426{-# INLINE[0] charIndexAt #-}
306{-# 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−29
@@ -1,29 +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 FlexibleInstances #-}
6−{-# LANGUAGE FunctionalDependencies #-}
7−{-# LANGUAGE UndecidableInstances #-}
8−module Reflex.Class.Switchable where
9−
10−import Control.Monad
11−import Reflex
12−
13−-- | Class representing things that can be switched when the provided event occurs
14−class Reflex t => Switchable t w | w -> t where
15− switching :: MonadHold t m => w -> Event t w -> m w
16−
17−instance Reflex t => Switchable t (Event t a) where
18− switching = switchHold
19−
20−instance Reflex t => Switchable t (Dynamic t a) where
21− switching a e = fmap join $ holdDyn a e
22−
23−instance Reflex t => Switchable t (Behavior t a) where
24− switching = switcher
25−
26−instance (Reflex t, Switchable t a, Switchable t b) => Switchable t (a, b) where
27− switching (a, b) e = (,)
28− <$> switching a (fmap fst e)
29− <*> switching b (fmap snd e)
src/Reflex/Spider/Orphans.hs−3
@@ -2,9 +2,6 @@
2Module: Reflex.Spider.Orphans
2Module: Reflex.Spider.Orphans
3Description: Orphan instances for SpiderTimeline and SpiderHost
3Description: Orphan instances for SpiderTimeline and SpiderHost
4-}
4-}
5−{-# LANGUAGE CPP #-}
6−{-# LANGUAGE FlexibleInstances #-}
7−{-# LANGUAGE MultiParamTypeClasses #-}
8{-# OPTIONS_GHC -fno-warn-orphans #-}
5{-# OPTIONS_GHC -fno-warn-orphans #-}
9module Reflex.Spider.Orphans where
6module Reflex.Spider.Orphans where
10
7
src/Reflex/Vty.hs+6
@@ -14,15 +14,21 @@ module Reflex.Vty
14 ( module Reflex
14 ( module Reflex
15 , module Reflex.Vty.Host
15 , module Reflex.Vty.Host
16 , module Reflex.Vty.Widget
16 , module Reflex.Vty.Widget
17+ , module Reflex.Vty.Widget.Box
17 , module Reflex.Vty.Widget.Input
18 , module Reflex.Vty.Widget.Input
18 , module Reflex.Vty.Widget.Layout
19 , module Reflex.Vty.Widget.Layout
20+ , module Reflex.Vty.Widget.Split
21+ , module Reflex.Vty.Widget.Text
19 , module Control.Monad.NodeId
22 , module Control.Monad.NodeId
20 ) where
23 ) where
21
24
22import Reflex
25import Reflex
23import Reflex.Vty.Host
26import Reflex.Vty.Host
24import Reflex.Vty.Widget
27import Reflex.Vty.Widget
28+import Reflex.Vty.Widget.Box
25import Reflex.Vty.Widget.Input
29import Reflex.Vty.Widget.Input
26import Reflex.Vty.Widget.Layout
30import Reflex.Vty.Widget.Layout
31+import Reflex.Vty.Widget.Split
32+import Reflex.Vty.Widget.Text
27
33
28import Control.Monad.NodeId
34import Control.Monad.NodeId
src/Reflex/Vty/Host.hs−7
@@ -2,12 +2,6 @@
2Module: Reflex.Vty.Host
2Module: Reflex.Vty.Host
3Description: Scaffolding for running a reflex-vty application
3Description: Scaffolding for running a reflex-vty application
4-}
4-}
5−{-# LANGUAGE ConstraintKinds #-}
6−{-# LANGUAGE FlexibleContexts #-}
7−{-# LANGUAGE GADTs #-}
8−{-# LANGUAGE LambdaCase #-}
9−{-# LANGUAGE RankNTypes #-}
10−
11module Reflex.Vty.Host
5module Reflex.Vty.Host
12 ( VtyApp
6 ( VtyApp
13 , VtyResult(..)
7 , VtyResult(..)
@@ -202,7 +196,6 @@ runVtyAppWithHandle vty vtyGuest = flip onException (V.shutdown vty) $
202 updateVty
196 updateVty
203 loop
197 loop
204 where
198 where
205− -- TODO Some part of this is probably general enough to belong in reflex
206 -- | Use the given 'FireCommand' to fire events that have subscribers
199 -- | Use the given 'FireCommand' to fire events that have subscribers
207 -- and call the callback for the 'TriggerInvocation' of each.
200 -- and call the callback for the 'TriggerInvocation' of each.
208 fireEventTriggerRefs
201 fireEventTriggerRefs
src/Reflex/Vty/Widget.hs+469−675
@@ -2,167 +2,41 @@
2Module: Reflex.Vty.Widget
2Module: Reflex.Vty.Widget
3Description: Basic set of widgets and building blocks for reflex-vty applications
3Description: Basic set of widgets and building blocks for reflex-vty applications
4-}
4-}
5−{-# LANGUAGE DefaultSignatures #-}
6−{-# LANGUAGE DeriveFunctor #-}
7−{-# LANGUAGE FlexibleContexts #-}
8−{-# LANGUAGE FlexibleInstances #-}
9−{-# LANGUAGE FunctionalDependencies #-}
10−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
11−{-# LANGUAGE LambdaCase #-}
12−{-# LANGUAGE RankNTypes #-}
13−{-# LANGUAGE RecursiveDo #-}
14−{-# LANGUAGE ScopedTypeVariables #-}
15−{-# LANGUAGE StandaloneDeriving #-}
16−{-# LANGUAGE TupleSections #-}
17−{-# LANGUAGE TypeFamilies #-}
18−{-# LANGUAGE UndecidableInstances #-}
5+{-# Language UndecidableInstances #-}
19−
6+
20−module Reflex.Vty.Widget
7+module Reflex.Vty.Widget where
21− ( VtyWidgetCtx(..)
22− , VtyWidget(..)
23− , VtyWidgetOut(..)
24− , ImageWriter(..)
25− , runVtyWidget
26− , mainWidget
27− , mainWidgetWithHandle
28− , HasDisplaySize(..)
29− , HasFocus(..)
30− , HasVtyInput(..)
31− , DynRegion(..)
32− -- TODO get rid of DynRegion?
33− , joinDynRegion
34− , dynRegion
35− , dynamicRegion
36− , currentRegion
37− , Region(..)
38− , regionSize
39− , regionBlankImage
40− , Drag(..)
41− , drag
42− , MouseDown(..)
43− , MouseUp(..)
44− , mouseDown
45− , mouseUp
46− , ScrollDirection(..)
47− , mouseScroll
48− , pane
49− , splitV
50− , splitH
51− , splitVDrag
52− , boxTitle
53− , box
54− , boxStatic
55− , RichTextConfig(..)
56− , richText
57− , text
58− , scrollableText
59− , display
60− , BoxStyle(..)
61− , hyphenBoxStyle
62− , singleBoxStyle
63− , roundedBoxStyle
64− , thickBoxStyle
65− , doubleBoxStyle
66− , fill
67− , hRule
68− , KeyCombo
69− , key
70− , keys
71− , keyCombo
72− , keyCombos
73− , blank
74− ) where
75
8
76import Control.Applicative (liftA2)
9import Control.Applicative (liftA2)
77−import Control.Monad (join)
78import Control.Monad.Fix (MonadFix)
10import Control.Monad.Fix (MonadFix)
79import Control.Monad.IO.Class (MonadIO)
11import Control.Monad.IO.Class (MonadIO)
12+import Control.Monad.Morph
13+import Control.Monad.NodeId
80−import Control.Monad.Reader (ReaderT, ask, asks, runReaderT)
14+import Control.Monad.Reader (ReaderT, ask, local, runReaderT)
15+import Control.Monad.Ref
81import Control.Monad.Trans (MonadTrans, lift)
16import Control.Monad.Trans (MonadTrans, lift)
82−import Data.Default (Default(..))
83import Data.Set (Set)
17import Data.Set (Set)
84import qualified Data.Set as Set
18import qualified Data.Set as Set
85−import Data.Text (Text)
86−import qualified Data.Text as T
87−import qualified Data.Text.Zipper as TZ
88import Graphics.Vty (Image)
19import Graphics.Vty (Image)
89import qualified Graphics.Vty as V
20import qualified Graphics.Vty as V
90import Reflex
21import Reflex
91import Reflex.Class ()
22import Reflex.Class ()
92import Reflex.Host.Class (MonadReflexCreateTrigger)
23import Reflex.Host.Class (MonadReflexCreateTrigger)
93−
94import Reflex.Vty.Host
24import Reflex.Vty.Host
95
25
96−import Control.Monad.NodeId
26+-- * Running a vty application
97−
98−-- | The context within which a 'VtyWidget' runs
99−data VtyWidgetCtx t = VtyWidgetCtx
100− { _vtyWidgetCtx_width :: Dynamic t Int
101− -- ^ The width of the region allocated to the widget.
102− , _vtyWidgetCtx_height :: Dynamic t Int
103− -- ^ The height of the region allocated to the widget.
104− , _vtyWidgetCtx_focus :: Dynamic t Bool
105− -- ^ Whether the widget should behave as if it has focus for keyboard input.
106− , _vtyWidgetCtx_input :: Event t VtyEvent
107− -- ^ User input events that the widget's parent chooses to share. These will generally
108− -- be filtered for relevance:
109− -- * Keyboard inputs are restricted to focused widgets
110− -- * Mouse inputs are restricted to the region in which the widget resides and are
111− -- translated into its internal coordinates.
112− }
113−
114−-- | The output of a 'VtyWidget'
115−data VtyWidgetOut t = VtyWidgetOut
116− { _vtyWidgetOut_shutdown :: Event t ()
117− }
118−
119−instance (Adjustable t m, MonadHold t m, Reflex t) => Adjustable t (VtyWidget t m) where
120− runWithReplace a0 a' = VtyWidget $ runWithReplace (unVtyWidget a0) $ fmap unVtyWidget a'
121− traverseIntMapWithKeyWithAdjust f dm0 dm' = VtyWidget $
122− traverseIntMapWithKeyWithAdjust (\k v -> unVtyWidget (f k v)) dm0 dm'
123− traverseDMapWithKeyWithAdjust f dm0 dm' = VtyWidget $ do
124− traverseDMapWithKeyWithAdjust (\k v -> unVtyWidget (f k v)) dm0 dm'
125− traverseDMapWithKeyWithAdjustWithMove f dm0 dm' = VtyWidget $ do
126− traverseDMapWithKeyWithAdjustWithMove (\k v -> unVtyWidget (f k v)) dm0 dm'
127−
128−-- | A widget that can read its context and produce image output
129−newtype VtyWidget t m a = VtyWidget
130− { unVtyWidget :: BehaviorWriterT t [Image] (ReaderT (VtyWidgetCtx t) m) a
131− } deriving
132− ( Functor
133− , Applicative
134− , Monad
135− , MonadSample t
136− , MonadHold t
137− , MonadFix
138− , NotReady t
139− , ImageWriter t
140− , PostBuild t
141− , TriggerEvent t
142− , MonadReflexCreateTrigger t
143− , MonadIO
144− )
145−
146−deriving instance PerformEvent t m => PerformEvent t (VtyWidget t m)
147−instance MonadTrans (VtyWidget t) where
148− lift f = VtyWidget $ lift $ lift f
149−
150−instance MonadNodeId m => MonadNodeId (VtyWidget t m) where
151− getNextNodeId = VtyWidget $ do
152− lift $ lift getNextNodeId
153−
154−-- | Runs a 'VtyWidget' with a given context
155−runVtyWidget
156− :: (Reflex t, MonadNodeId m)
157− => VtyWidgetCtx t
158− -> VtyWidget t m a
159− -> m (a, Behavior t [Image])
160−runVtyWidget ctx w = runReaderT (runBehaviorWriterT (unVtyWidget w)) ctx
161
27
162−-- | 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
163mainWidgetWithHandle
29mainWidgetWithHandle
164 :: V.Vty
30 :: V.Vty
31+ -> (forall t m.
165− -> (forall t m. (MonadVtyApp t m, MonadNodeId m) => VtyWidget t m (Event t ()))
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 ()))
166 -> IO ()
40 -> IO ()
167mainWidgetWithHandle vty child =
41mainWidgetWithHandle vty child =
168 runVtyAppWithHandle vty $ \dr0 inp -> do
42 runVtyAppWithHandle vty $ \dr0 inp -> do
@@ -172,71 +46,178 @@ mainWidgetWithHandle vty child =
172 let inp' = fforMaybe inp $ \case
46 let inp' = fforMaybe inp $ \case
173 V.EvResize {} -> Nothing
47 V.EvResize {} -> Nothing
174 x -> Just x
48 x -> Just x
175− let ctx = VtyWidgetCtx
49+ (shutdown, images) <- runThemeReader (constant V.defAttr) $
176− { _vtyWidgetCtx_width = fmap fst size
50+ runFocusReader (pure True) $
177− , _vtyWidgetCtx_height = fmap snd size
51+ runDisplayRegion (fmap (\(w, h) -> Region 0 0 w h) size) $
178− , _vtyWidgetCtx_input = inp'
52+ runImageWriter $
179− , _vtyWidgetCtx_focus = constDyn True
53+ runNodeIdT $
180− }
54+ runInput inp' $ do
181− (shutdown, images) <- runNodeIdT $ runVtyWidget ctx $ do
182− tellImages . ffor (current size) $ \(w, h) -> [V.charFill V.defAttr ' ' w h]
55+ tellImages . ffor (current size) $ \(w, h) -> [V.charFill V.defAttr ' ' w h]
183− child
56+ child
184 return $ VtyResult
57 return $ VtyResult
185 { _vtyResult_picture = fmap (V.picForLayers . reverse) images
58 { _vtyResult_picture = fmap (V.picForLayers . reverse) images
186 , _vtyResult_shutdown = shutdown
59 , _vtyResult_shutdown = shutdown
187 }
60 }
188
61
62+-- | The output of a vty widget
63+data VtyWidgetOut t = VtyWidgetOut
64+ { _vtyWidgetOut_shutdown :: Event t ()
65+ }
66+
189-- | Like 'mainWidgetWithHandle', but uses a default vty configuration
67-- | Like 'mainWidgetWithHandle', but uses a default vty configuration
190mainWidget
68mainWidget
69+ :: (forall t m.
191− :: (forall t m. (MonadVtyApp t m, MonadNodeId m) => VtyWidget t m (Event t ()))
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 ()))
192 -> IO ()
78 -> IO ()
193mainWidget child = do
79mainWidget child = do
194 vty <- getDefaultVty
80 vty <- getDefaultVty
195 mainWidgetWithHandle vty child
81 mainWidgetWithHandle vty child
196
82
197−-- | A class for things that know their own display size dimensions
83+-- * Input Events
198−class (Reflex t, Monad m) => HasDisplaySize t m | m -> t where
199− -- | Retrieve the display width (columns)
200− displayWidth :: m (Dynamic t Int)
201− default displayWidth :: (f m' ~ m, MonadTrans f, HasDisplaySize t m') => m (Dynamic t Int)
202− displayWidth = lift displayWidth
203− -- | Retrieve the display height (rows)
204− displayHeight :: m (Dynamic t Int)
205− default displayHeight :: (f m' ~ m, MonadTrans f, HasDisplaySize t m') => m (Dynamic t Int)
206− displayHeight = lift displayHeight
207−
208−instance (Reflex t, Monad m) => HasDisplaySize t (VtyWidget t m) where
209− displayWidth = VtyWidget . lift $ asks _vtyWidgetCtx_width
210− displayHeight = VtyWidget . lift $ asks _vtyWidgetCtx_height
211−
212−instance HasDisplaySize t m => HasDisplaySize t (ReaderT x m)
213−instance HasDisplaySize t m => HasDisplaySize t (BehaviorWriterT t x m)
214−instance HasDisplaySize t m => HasDisplaySize t (DynamicWriterT t x m)
215−instance HasDisplaySize t m => HasDisplaySize t (EventWriterT t x m)
216−
217−instance HasDisplaySize t m => HasDisplaySize t (NodeIdT m)
218
84
219-- | A class for things that can receive vty events as input
85-- | A class for things that can receive vty events as input
220−class HasVtyInput t m | m -> t where
86+class HasInput t m | m -> t where
221 input :: m (Event t VtyEvent)
87 input :: m (Event t VtyEvent)
88+ default input :: (f m' ~ m, Monad m', MonadTrans f, HasInput t m') => m (Event t VtyEvent)
89+ input = lift input
90+ -- | User input events that the widget's parent chooses to share. These will generally
91+ -- be filtered for relevance.
92+ 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+ )
222
113
223−instance (Reflex t, Monad m) => HasVtyInput t (VtyWidget t m) where
114+instance (Adjustable t m, MonadHold t m, Reflex t) => Adjustable t (Input t m) where
224− input = VtyWidget . lift $ asks _vtyWidgetCtx_input
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
225
149
226−-- | A class for things that can dynamically gain and lose focus
150+-- | Type synonym for a key and modifier combination
227−class HasFocus t m | m -> t where
151+type KeyCombo = (V.Key, [V.Modifier])
228− focus :: m (Dynamic t Bool)
229
152
230−instance (Reflex t, Monad m) => HasFocus t (VtyWidget t m) where
153+-- | Emits an event that fires on a particular key press (without modifiers)
231− focus = VtyWidget . lift $ asks _vtyWidgetCtx_focus
154+key :: (Monad m, Reflex t, HasInput t m) => V.Key -> m (Event t KeyCombo)
155+key = keyCombos . Set.singleton . (,[])
232
156
233−-- | A class for widgets that can produce images to draw to the display
157+-- | Emits an event that fires on particular key presses (without modifiers)
234−class (Reflex t, Monad m) => ImageWriter t m | m -> t where
158+keys :: (Monad m, Reflex t, HasInput t m) => [V.Key] -> m (Event t KeyCombo)
235− -- | Send images upstream for rendering
159+keys = keyCombos . Set.fromList . fmap (,[])
236− tellImages :: Behavior t [Image] -> m ()
237
160
238−instance (Monad m, Reflex t) => ImageWriter t (BehaviorWriterT t [Image] m) where
161+-- | Emit an event that fires whenever the provided key combination occurs
239− tellImages = tellBehavior
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
180+
181+-- | Filter the keyboard input that a child widget may receive
182+filterKeys :: (Reflex t, HasInput t m) => (KeyCombo -> Bool) -> m a -> m a
183+filterKeys f x = localInput (ffilter (\case
184+ V.EvKey k mods -> f (k, mods)
185+ _ -> True)) x
186+
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
240
221
241-- | A chunk of the display area
222-- | A chunk of the display area
242data Region = Region
223data Region = Region
@@ -247,521 +228,334 @@ data Region = Region
247 }
228 }
248 deriving (Show, Read, Eq, Ord)
229 deriving (Show, Read, Eq, Ord)
249
230
250−-- | A dynamic chunk of the display area
231+-- | A region that occupies no space.
251−data DynRegion t = DynRegion
232+nilRegion :: Region
252− { _dynRegion_left :: Dynamic t Int
233+nilRegion = Region 0 0 0 0
253− , _dynRegion_top :: Dynamic t Int
254− , _dynRegion_width :: Dynamic t Int
255− , _dynRegion_height :: Dynamic t Int
256− }
257
234
258-- | The width and height of a 'Region'
235-- | The width and height of a 'Region'
259regionSize :: Region -> (Int, Int)
236regionSize :: Region -> (Int, Int)
260regionSize (Region _ _ w h) = (w, h)
237regionSize (Region _ _ w h) = (w, h)
261
238
262-- | Produces an 'Image' that fills a region with space characters
239-- | Produces an 'Image' that fills a region with space characters
263−regionBlankImage :: Region -> Image
240+regionBlankImage :: V.Attr -> Region -> Image
264−regionBlankImage r@(Region _ _ width height) =
241+regionBlankImage attr r@(Region _ _ width height) =
265− withinImage r $ V.charFill V.defAttr ' ' width height
242+ withinImage r $ V.charFill attr ' ' width height
266−
267−-- | A behavior of the current display area represented by a 'DynRegion'
268−currentRegion :: Reflex t => DynRegion t -> Behavior t Region
269−currentRegion (DynRegion l t w h) = Region <$> current l <*> current t <*> current w <*> current h
270−
271−joinDynRegion :: Reflex t => Dynamic t (DynRegion t) -> DynRegion t
272−joinDynRegion d = DynRegion
273− (_dynRegion_left =<< d)
274− (_dynRegion_top =<< d)
275− (_dynRegion_width =<< d)
276− (_dynRegion_height =<< d)
277−
278−dynRegion :: Reflex t => Dynamic t Region -> DynRegion t
279−dynRegion r = DynRegion
280− { _dynRegion_left = _region_left <$> r
281− , _dynRegion_top = _region_top <$> r
282− , _dynRegion_width = _region_width <$> r
283− , _dynRegion_height = _region_height <$> r
284− }
285
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
286−dynamicRegion :: Reflex t => DynRegion t -> Dynamic t Region
247+ askRegion :: m (Dynamic t Region)
287−dynamicRegion d = Region
248+ default askRegion :: (f m' ~ m, MonadTrans f, HasDisplayRegion t m') => m (Dynamic t Region)
288− <$> _dynRegion_left d
249+ askRegion = lift askRegion
289− <*> _dynRegion_top d
250+ -- | Run an action in a local region, by applying a transformation to the region
290− <*> _dynRegion_width d
251+ localRegion :: (Dynamic t Region -> Dynamic t Region) -> m a -> m a
291− <*> _dynRegion_height d
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+ )
292
282
293−-- | Translates and crops an 'Image' so that it is contained by
283+instance (Monad m, Reflex t) => HasDisplayRegion t (DisplayRegion t m) where
294−-- the given 'Region'.
284+ askRegion = DisplayRegion ask
295−withinImage
285+ localRegion f = DisplayRegion . local f . unDisplayRegion
296− :: Region
297− -> Image
298− -> Image
299−withinImage (Region left top width height)
300− | width < 0 || height < 0 = withinImage (Region left top 0 0)
301− | otherwise = V.translate left top . V.crop width height
302
286
303−-- | Low-level widget combinator that runs a child 'VtyWidget' within
287+deriving instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (DisplayRegion t m)
304−-- a given region and context. This widget filters and modifies the input
288+deriving instance NotReady t m => NotReady t (DisplayRegion t m)
305−-- that the child widget receives such that:
289+deriving instance PerformEvent t m => PerformEvent t (DisplayRegion t m)
306−-- * unfocused widgets receive no key events
290+deriving instance PostBuild t m => PostBuild t (DisplayRegion t m)
307−-- * mouse inputs outside the region are ignored
291+deriving instance TriggerEvent t m => TriggerEvent t (DisplayRegion t m)
308−-- * mouse inputs inside the region have their coordinates translated such
292+instance HasImageWriter t m => HasImageWriter t (DisplayRegion t m)
309−-- that (0,0) is the top-left corner of the region
293+instance HasFocusReader t m => HasFocusReader t (DisplayRegion t m)
310−pane
311− :: (Reflex t, Monad m, MonadNodeId m)
312− => DynRegion t
313− -> Dynamic t Bool -- ^ Whether the widget should be focused when the parent is.
314− -> VtyWidget t m a
315− -> VtyWidget t m a
316−pane dr foc child = VtyWidget $ do
317− ctx <- lift ask
318− let reg = currentRegion dr
319− let ctx' = VtyWidgetCtx
320− { _vtyWidgetCtx_input = leftmost -- TODO: think about this leftmost more.
321− [ fmapMaybe id $
322− attachWith (\(r,f) e -> filterInput r f e)
323− (liftA2 (,) reg (current foc))
324− (_vtyWidgetCtx_input ctx)
325− ]
326− , _vtyWidgetCtx_focus = liftA2 (&&) (_vtyWidgetCtx_focus ctx) foc
327− , _vtyWidgetCtx_width = _dynRegion_width dr
328− , _vtyWidgetCtx_height = _dynRegion_height dr
329− }
330− (result, images) <- lift . lift $ runVtyWidget ctx' child
331− let images' = liftA2 (\r is -> map (withinImage r) is) reg images
332− tellImages images'
333− return result
334− where
335− filterInput :: Region -> Bool -> VtyEvent -> Maybe VtyEvent
336− filterInput (Region l t w h) focused e = case e of
337− V.EvKey _ _ | not focused -> Nothing
338− V.EvMouseDown x y btn m -> mouse (\u v -> V.EvMouseDown u v btn m) x y
339− V.EvMouseUp x y btn -> mouse (\u v -> V.EvMouseUp u v btn) x y
340− _ -> Just e
341− where
342− mouse con x y
343− | or [ x < l
344− , y < t
345− , x >= l + w
346− , y >= t + h ] = Nothing
347− | otherwise =
348− Just (con (x - l) (y - t))
349−
350−-- | Information about a drag operation
351−data Drag = Drag
352− { _drag_from :: (Int, Int) -- ^ Where the drag began
353− , _drag_to :: (Int, Int) -- ^ Where the mouse currently is
354− , _drag_button :: V.Button -- ^ Which mouse button is dragging
355− , _drag_modifiers :: [V.Modifier] -- ^ What modifiers are held
356− , _drag_end :: Bool -- ^ Whether the drag ended (the mouse button was released)
357− }
358− deriving (Eq, Ord, Show)
359−
360−-- | Converts raw vty mouse drag events into an event stream of 'Drag's
361−drag
362− :: (Reflex t, MonadFix m, MonadHold t m)
363− => V.Button
364− -> VtyWidget t m (Event t Drag)
365−drag btn = do
366− inp <- input
367− let f :: Maybe Drag -> V.Event -> Maybe Drag
368− f Nothing = \case
369− V.EvMouseDown x y btn' mods
370− | btn == btn' -> Just $ Drag (x,y) (x,y) btn' mods False
371− | otherwise -> Nothing
372− _ -> Nothing
373− f (Just (Drag from _ _ mods end)) = \case
374− V.EvMouseDown x y btn' mods'
375− | end && btn == btn' -> Just $ Drag (x,y) (x,y) btn' mods' False
376− | btn == btn' -> Just $ Drag from (x,y) btn mods' False
377− | otherwise -> Nothing -- Ignore other buttons.
378− V.EvMouseUp x y (Just btn')
379− | end -> Nothing
380− | btn == btn' -> Just $ Drag from (x,y) btn mods True
381− | otherwise -> Nothing
382− V.EvMouseUp x y Nothing -- Terminal doesn't specify mouse up button,
383− -- assume it's the right one.
384− | end -> Nothing
385− | otherwise -> Just $ Drag from (x,y) btn mods True
386− _ -> Nothing
387− rec let newDrag = attachWithMaybe f (current dragD) inp
388− dragD <- holdDyn Nothing $ Just <$> newDrag
389− return (fmapMaybe id $ updated dragD)
390−
391−-- | Mouse down events for a particular mouse button
392−mouseDown
393− :: (Reflex t, Monad m)
394− => V.Button
395− -> VtyWidget t m (Event t MouseDown)
396−mouseDown btn = do
397− i <- input
398− return $ fforMaybe i $ \case
399− V.EvMouseDown x y btn' mods -> if btn == btn'
400− then Just $ MouseDown btn' (x, y) mods
401− else Nothing
402− _ -> Nothing
403
294
404−-- | Mouse up events for a particular mouse button
295+instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (DisplayRegion t m) where
405−mouseUp
296+ runWithReplace (DisplayRegion a) e = DisplayRegion $ runWithReplace a $ fmap unDisplayRegion e
406− :: (Reflex t, Monad m)
297+ traverseIntMapWithKeyWithAdjust f m e = DisplayRegion $ traverseIntMapWithKeyWithAdjust (\k v -> unDisplayRegion $ f k v) m e
407− => VtyWidget t m (Event t MouseUp)
298+ traverseDMapWithKeyWithAdjust f m e = DisplayRegion $ traverseDMapWithKeyWithAdjust (\k v -> unDisplayRegion $ f k v) m e
408−mouseUp = do
299+ traverseDMapWithKeyWithAdjustWithMove f m e = DisplayRegion $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unDisplayRegion $ f k v) m e
409− i <- input
410− return $ fforMaybe i $ \case
411− V.EvMouseUp x y btn' -> Just $ MouseUp btn' (x, y)
412− _ -> Nothing
413
300
414−-- | Information about a mouse down event
301+instance MonadTrans (DisplayRegion t) where
415−data MouseDown = MouseDown
302+ lift = DisplayRegion . lift
416− { _mouseDown_button :: V.Button
417− , _mouseDown_coordinates :: (Int, Int)
418− , _mouseDown_modifiers :: [V.Modifier]
419− }
420− deriving (Eq, Ord, Show)
421
303
422−-- | Information about a mouse up event
304+instance MFunctor (DisplayRegion t) where
423−data MouseUp = MouseUp
305+ hoist f = DisplayRegion . hoist f . unDisplayRegion
424− { _mouseUp_button :: Maybe V.Button
425− , _mouseUp_coordinates :: (Int, Int)
426− }
427− deriving (Eq, Ord, Show)
428
306
429−-- | Mouse scroll direction
307+instance MonadNodeId m => MonadNodeId (DisplayRegion t m)
430−data ScrollDirection = ScrollDirection_Up | ScrollDirection_Down
431− deriving (Eq, Ord, Show)
432
308
433−-- | Produce an event that fires when the mouse wheel is scrolled
309+-- | Run a 'DisplayRegion' action with a given 'Region'
434−mouseScroll
310+runDisplayRegion
435 :: (Reflex t, Monad m)
311 :: (Reflex t, Monad m)
436− => VtyWidget t m (Event t ScrollDirection)
312+ => Dynamic t Region
437−mouseScroll = do
313+ -> DisplayRegion t m a
438− up <- mouseDown V.BScrollUp
314+ -> m a
439− down <- mouseDown V.BScrollDown
315+runDisplayRegion r = flip runReaderT r . unDisplayRegion
440− return $ leftmost
441− [ ScrollDirection_Up <$ up
442− , ScrollDirection_Down <$ down
443− ]
444
316
445−-- | Type synonym for a key and modifier combination
317+-- * Getting focus state
446−type KeyCombo = (V.Key, [V.Modifier])
447
318
448−-- | Emits an event that fires on a particular key press (without modifiers)
319+-- | A class for things that can dynamically gain and lose focus
449−key :: (Monad m, Reflex t) => V.Key -> VtyWidget t m (Event t KeyCombo)
320+class (Reflex t, Monad m) => HasFocusReader t m | m -> t where
450−key = keyCombos . Set.singleton . (,[])
321+ focus :: m (Dynamic t Bool)
322+ default focus :: (f m' ~ m, Monad m', MonadTrans f, HasFocusReader t m') => m (Dynamic t Bool)
323+ 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+ )
451
347
452−-- | Emits an event that fires on particular key presses (without modifiers)
348+instance (Monad m, Reflex t) => HasFocusReader t (FocusReader t m) where
453−keys :: (Monad m, Reflex t) => [V.Key] -> VtyWidget t m (Event t KeyCombo)
349+ focus = FocusReader ask
454−keys = keyCombos . Set.fromList . fmap (,[])
350+ localFocus f = FocusReader . local f . unFocusReader
455
351
456−-- | Emit an event that fires whenever the provided key combination occurs
352+deriving instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (FocusReader t m)
457−keyCombo
353+deriving instance NotReady t m => NotReady t (FocusReader t m)
458− :: (Reflex t, Monad m)
354+deriving instance PerformEvent t m => PerformEvent t (FocusReader t m)
459− => KeyCombo
355+deriving instance PostBuild t m => PostBuild t (FocusReader t m)
460− -> VtyWidget t m (Event t KeyCombo)
356+deriving instance TriggerEvent t m => TriggerEvent t (FocusReader t m)
461−keyCombo = keyCombos . Set.singleton
357+instance HasImageWriter t m => HasImageWriter t (FocusReader t m)
462
358
463−-- | Emit an event that fires whenever any of the provided key combinations occur
359+instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (FocusReader t m) where
464−keyCombos
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
465 :: (Reflex t, Monad m)
375 :: (Reflex t, Monad m)
466− => Set KeyCombo
376+ => Dynamic t Bool
467− -> VtyWidget t m (Event t KeyCombo)
377+ -> FocusReader t m a
468−keyCombos ks = do
378+ -> m a
469− i <- input
379+runFocusReader b = flip runReaderT b . unFocusReader
470− return $ fforMaybe i $ \case
471− V.EvKey k m -> if Set.member (k, m) ks
472− then Just (k, m)
473− else Nothing
474− _ -> Nothing
475
380
476−-- | A plain split of the available space into vertically stacked panes.
381+-- * "Image" output
477−-- No visual separator is built in here.
478−splitV :: (Reflex t, Monad m, MonadNodeId m)
479− => Dynamic t (Int -> Int)
480− -- ^ Function used to determine size of first pane based on available size
481− -> Dynamic t (Bool, Bool)
482− -- ^ How to focus the two sub-panes, given that we are focused.
483− -> VtyWidget t m a
484− -- ^ Widget for first pane
485− -> VtyWidget t m b
486− -- ^ Widget for second pane
487− -> VtyWidget t m (a,b)
488−splitV sizeFunD focD wA wB = do
489− dw <- displayWidth
490− dh <- displayHeight
491− let regA = DynRegion
492− { _dynRegion_left = pure 0
493− , _dynRegion_top = pure 0
494− , _dynRegion_width = dw
495− , _dynRegion_height = sizeFunD <*> dh
496− }
497− regB = DynRegion
498− { _dynRegion_left = pure 0
499− , _dynRegion_top = _dynRegion_height regA
500− , _dynRegion_width = dw
501− , _dynRegion_height = liftA2 (-) dh (_dynRegion_height regA)
502− }
503− ra <- pane regA (fst <$> focD) wA
504− rb <- pane regB (snd <$> focD) wB
505− return (ra,rb)
506−
507−-- | A plain split of the available space into horizontally stacked panes.
508−-- No visual separator is built in here.
509−splitH :: (Reflex t, Monad m, MonadNodeId m)
510− => Dynamic t (Int -> Int)
511− -- ^ Function used to determine size of first pane based on available size
512− -> Dynamic t (Bool, Bool)
513− -- ^ How to focus the two sub-panes, given that we are focused.
514− -> VtyWidget t m a
515− -- ^ Widget for first pane
516− -> VtyWidget t m b
517− -- ^ Widget for second pane
518− -> VtyWidget t m (a,b)
519−splitH sizeFunD focD wA wB = do
520− dw <- displayWidth
521− dh <- displayHeight
522− let regA = DynRegion
523− { _dynRegion_left = pure 0
524− , _dynRegion_top = pure 0
525− , _dynRegion_width = sizeFunD <*> dw
526− , _dynRegion_height = dh
527− }
528− regB = DynRegion
529− { _dynRegion_left = _dynRegion_width regA
530− , _dynRegion_top = pure 0
531− , _dynRegion_width = liftA2 (-) dw (_dynRegion_width regA)
532− , _dynRegion_height = dh
533− }
534− liftA2 (,) (pane regA (fmap fst focD) wA) (pane regB (fmap snd focD) wB)
535−
536−-- | A split of the available space into two parts with a draggable separator.
537−-- Starts with half the space allocated to each, and the first pane has focus.
538−-- Clicking in a pane switches focus.
539−splitVDrag :: (Reflex t, MonadFix m, MonadHold t m, MonadNodeId m)
540− => VtyWidget t m ()
541− -> VtyWidget t m a
542− -> VtyWidget t m b
543− -> VtyWidget t m (a,b)
544−splitVDrag wS wA wB = do
545− dh <- displayHeight
546− dw <- displayWidth
547− h0 <- sample $ current dh -- TODO
548− dragE <- drag V.BLeft
549− let splitter0 = h0 `div` 2
550− rec splitterCheckpoint <- holdDyn splitter0 $ leftmost [fst <$> ffilter snd dragSplitter, resizeSplitter]
551− splitterPos <- holdDyn splitter0 $ leftmost [fst <$> dragSplitter, resizeSplitter]
552− splitterFrac <- holdDyn ((1::Double) / 2) $ ffor (attach (current dh) (fst <$> dragSplitter)) $ \(h, x) ->
553− fromIntegral x / max 1 (fromIntegral h)
554− let dragSplitter = fforMaybe (attach (current splitterCheckpoint) dragE) $
555− \(splitterY, Drag (_, fromY) (_, toY) _ _ end) ->
556− if splitterY == fromY then Just (toY, end) else Nothing
557− regA = DynRegion 0 0 dw splitterPos
558− regS = DynRegion 0 splitterPos dw 1
559− regB = DynRegion 0 (splitterPos + 1) dw (dh - splitterPos - 1)
560− resizeSplitter = ffor (attach (current splitterFrac) (updated dh)) $
561− \(frac, h) -> round (frac * fromIntegral h)
562− focA <- holdDyn True $ leftmost
563− [ True <$ mA
564− , False <$ mB
565− ]
566− (mA, rA) <- pane regA focA $ withMouseDown wA
567− pane regS (pure False) wS
568− (mB, rB) <- pane regB (not <$> focA) $ withMouseDown wB
569− return (rA, rB)
570− where
571− withMouseDown x = do
572− m <- mouseDown V.BLeft
573− x' <- x
574− return (m, x')
575−
576−-- | Fill the background with a particular character.
577−fill :: (Reflex t, Monad m) => Char -> VtyWidget t m ()
578−fill c = do
579− dw <- displayWidth
580− dh <- displayHeight
581− let fillImg = current $ liftA2 (\w h -> [V.charFill V.defAttr c w h]) dw dh
582− tellImages fillImg
583−
584−-- | Fill the background with the bottom
585−hRule :: (Reflex t, Monad m) => BoxStyle -> VtyWidget t m ()
586−hRule boxStyle = fill (_boxStyle_s boxStyle)
587−
588−-- | Defines a set of symbols to use to draw the outlines of boxes
589−-- C.f. https://en.wikipedia.org/wiki/Box-drawing_character
590−data BoxStyle = BoxStyle
591− { _boxStyle_nw :: Char
592− , _boxStyle_n :: Char
593− , _boxStyle_ne :: Char
594− , _boxStyle_e :: Char
595− , _boxStyle_se :: Char
596− , _boxStyle_s :: Char
597− , _boxStyle_sw :: Char
598− , _boxStyle_w :: Char
599− }
600
382
601−instance Default BoxStyle where
383+-- | A class for widgets that can produce images to draw to the display
602− def = singleBoxStyle
384+class (Reflex t, Monad m) => HasImageWriter t m | m -> t where
603−
385+ -- | Send images upstream for rendering
604−-- | A box style that uses hyphens and pipe characters. Doesn't handle
386+ tellImages :: Behavior t [Image] -> m ()
605−-- corners very well.
387+ default tellImages :: (f m' ~ m, Monad m', MonadTrans f, HasImageWriter t m') => Behavior t [Image] -> m ()
606−hyphenBoxStyle :: BoxStyle
388+ tellImages = lift . tellImages
607−hyphenBoxStyle = BoxStyle '-' '-' '-' '|' '-' '-' '-' '|'
389+ -- | Apply a transformation to the images produced by the child actions
608−
390+ mapImages :: (Behavior t [Image] -> Behavior t [Image]) -> m a -> m a
609−-- | A single line box style
391+ default mapImages :: (f m' ~ m, Monad m', MFunctor f, HasImageWriter t m') => (Behavior t [Image] -> Behavior t [Image]) -> m a -> m a
610−singleBoxStyle :: BoxStyle
392+ mapImages f = hoist (mapImages f)
611−singleBoxStyle = BoxStyle '┌' '─' '┐' '│' '┘' '─' '└' '│'
393+
612−
394+-- | A widget that can produce images to draw onto the display
613−-- | A thick single line box style
395+newtype ImageWriter t m a = ImageWriter
614−thickBoxStyle :: BoxStyle
396+ { unImageWriter :: BehaviorWriterT t [Image] m a }
615−thickBoxStyle = BoxStyle '┏' '━' '┓' '┃' '┛' '━' '┗' '┃'
397+ deriving
616−
398+ ( Functor
617−-- | A double line box style
399+ , Applicative
618−doubleBoxStyle :: BoxStyle
400+ , Monad
619−doubleBoxStyle = BoxStyle '╔' '═' '╗' '║' '╝' '═' '╚' '║'
401+ , MonadFix
620−
402+ , MonadHold t
621−-- | A single line box style with rounded corners
403+ , MonadIO
622−roundedBoxStyle :: BoxStyle
404+ , MonadRef
623−roundedBoxStyle = BoxStyle '╭' '─' '╮' '│' '╯' '─' '╰' '│'
405+ , MonadReflexCreateTrigger t
624−
406+ , MonadSample t
625−-- | Draws a titled box in the provided style and a child widget inside of that box
407+ , NotReady t
626−boxTitle :: (Monad m, Reflex t, MonadNodeId m)
408+ , PerformEvent t
627− => Behavior t BoxStyle
409+ , PostBuild t
628− -> Text
410+ , TriggerEvent t
629− -> VtyWidget t m a
411+ )
630− -> VtyWidget t m a
412+
631−boxTitle boxStyle title child = do
413+instance MonadTrans (ImageWriter t) where
632− dh <- displayHeight
414+ lift = ImageWriter . lift
633− dw <- displayWidth
415+
634− let boxReg = DynRegion (pure 0) (pure 0) dw dh
416+instance MFunctor (ImageWriter t) where
635− innerReg = DynRegion (pure 1) (pure 1) (subtract 2 <$> dw) (subtract 2 <$> dh)
417+ hoist f = ImageWriter . (hoist f) . unImageWriter
636− tellImages (boxImages <$> boxStyle <*> currentRegion boxReg)
418+
637− tellImages (fmap (\r -> [regionBlankImage r]) (currentRegion innerReg))
419+instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (ImageWriter t m) where
638− pane innerReg (pure True) child
420+ runWithReplace (ImageWriter a) e = ImageWriter $ runWithReplace a $ fmap unImageWriter e
639− where
421+ traverseIntMapWithKeyWithAdjust f m e = ImageWriter $ traverseIntMapWithKeyWithAdjust (\k v -> unImageWriter $ f k v) m e
640− boxImages :: BoxStyle -> Region -> [Image]
422+ traverseDMapWithKeyWithAdjust f m e = ImageWriter $ traverseDMapWithKeyWithAdjust (\k v -> unImageWriter $ f k v) m e
641− boxImages style (Region left top width height) =
423+ traverseDMapWithKeyWithAdjustWithMove f m e = ImageWriter $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unImageWriter $ f k v) m e
642− let right = left + width - 1
643− bottom = top + height - 1
644− sides =
645− [ withinImage (Region (left + 1) top (width - 2) 1) $
646− V.text' V.defAttr $
647− hPadText title (_boxStyle_n style) (width - 2)
648− , withinImage (Region right (top + 1) 1 (height - 2)) $
649− V.charFill V.defAttr (_boxStyle_e style) 1 (height - 2)
650− , withinImage (Region (left + 1) bottom (width - 2) 1) $
651− V.charFill V.defAttr (_boxStyle_s style) (width - 2) 1
652− , withinImage (Region left (top + 1) 1 (height - 2)) $
653− V.charFill V.defAttr (_boxStyle_w style) 1 (height - 2)
654− ]
655− corners =
656− [ withinImage (Region left top 1 1) $
657− V.char V.defAttr (_boxStyle_nw style)
658− , withinImage (Region right top 1 1) $
659− V.char V.defAttr (_boxStyle_ne style)
660− , withinImage (Region right bottom 1 1) $
661− V.char V.defAttr (_boxStyle_se style)
662− , withinImage (Region left bottom 1 1) $
663− V.char V.defAttr (_boxStyle_sw style)
664− ]
665− in sides ++ if width > 1 && height > 1 then corners else []
666− hPadText :: T.Text -> Char -> Int -> T.Text
667− hPadText t c l = if lt >= l
668− then t
669− else left <> t <> right
670− where
671− lt = T.length t
672− delta = l - lt
673− mkHalf n = T.replicate (n `div` 2) (T.singleton c)
674− left = mkHalf $ delta + 1
675− right = mkHalf delta
676−
677−-- | A box without a title
678−box :: (Monad m, Reflex t, MonadNodeId m)
679− => Behavior t BoxStyle
680− -> VtyWidget t m a
681− -> VtyWidget t m a
682−box boxStyle = boxTitle boxStyle mempty
683−
684−-- | A box whose style is static
685−boxStatic
686− :: (Reflex t, Monad m, MonadNodeId m)
687− => BoxStyle
688− -> VtyWidget t m a
689− -> VtyWidget t m a
690−boxStatic = box . pure
691−
692−-- | Configuration options for displaying "rich" text
693−data RichTextConfig t = RichTextConfig
694− { _richTextConfig_attributes :: Behavior t V.Attr
695− }
696
424
697−instance Reflex t => Default (RichTextConfig t) where
425+instance HasImageWriter t m => HasImageWriter t (ReaderT x m)
698− def = RichTextConfig $ pure V.defAttr
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)
699
430
700−-- | A widget that displays text with custom time-varying attributes
431+instance (Monad m, Reflex t) => HasImageWriter t (ImageWriter t m) where
701−richText
432+ tellImages = ImageWriter . tellBehavior
433+ mapImages f (ImageWriter x) = ImageWriter $ do
434+ (a, images) <- lift $ runBehaviorWriterT x
435+ tellBehavior $ f images
436+ pure a
437+
438+instance HasDisplayRegion t m => HasDisplayRegion t (ImageWriter t m)
439+instance HasFocusReader t m => HasFocusReader t (ImageWriter t m)
440+
441+-- | Run a widget that can produce images
442+runImageWriter
702 :: (Reflex t, Monad m)
443 :: (Reflex t, Monad m)
703− => RichTextConfig t
444+ => ImageWriter t m a
704− -> Behavior t Text
445+ -> m (a, Behavior t [Image])
705− -> VtyWidget t m ()
446+runImageWriter = runBehaviorWriterT . unImageWriter
706−richText cfg t = do
447+
707− dw <- displayWidth
448+-- * Theming
708− let img = (\w a s -> [wrapText w a s])
449+
709− <$> current dw
450+-- | A class for things that can be visually styled
710− <*> _richTextConfig_attributes cfg
451+class (Reflex t, Monad m) => HasTheme t m | m -> t where
711− <*> t
452+ theme :: m (Behavior t V.Attr)
712− tellImages img
453+ default theme :: (f m' ~ m, Monad m', MonadTrans f, HasTheme t m') => m (Behavior t V.Attr)
713− where
454+ theme = lift theme
714− wrapText maxWidth attrs = V.vertCat
455+ localTheme :: (Behavior t V.Attr -> Behavior t V.Attr) -> m a -> m a
715− . concatMap (fmap (V.string attrs . T.unpack) . TZ.wrapWithOffset maxWidth 0)
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
716− . T.split (=='\n')
457+ localTheme f = hoist (localTheme f)
458+
459+instance HasTheme t m => HasTheme t (ReaderT x m)
460+instance HasTheme t m => HasTheme t (BehaviorWriterT t x m)
461+instance HasTheme t m => HasTheme t (DynamicWriterT t x m)
462+instance HasTheme t m => HasTheme t (EventWriterT t x m)
463+instance HasTheme t m => HasTheme t (NodeIdT m)
464+instance HasTheme t m => HasTheme t (Input t m)
465+instance HasTheme t m => HasTheme t (ImageWriter t m)
466+instance HasTheme t m => HasTheme t (DisplayRegion t m)
467+instance HasTheme t m => HasTheme t (FocusReader t m)
468+
469+-- | A widget that has access to theme information
470+newtype ThemeReader t m a = ThemeReader
471+ { unThemeReader :: ReaderT (Behavior t V.Attr) m a }
472+ deriving
473+ ( Functor
474+ , Applicative
475+ , Monad
476+ , MonadFix
477+ , MonadHold t
478+ , MonadIO
479+ , MonadRef
480+ , MonadSample t
481+ )
482+
483+instance (Monad m, Reflex t) => HasTheme t (ThemeReader t m) where
484+ theme = ThemeReader ask
485+ localTheme f = ThemeReader . local f . unThemeReader
486+
487+deriving instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (ThemeReader t m)
488+deriving instance NotReady t m => NotReady t (ThemeReader t m)
489+deriving instance PerformEvent t m => PerformEvent t (ThemeReader t m)
490+deriving instance PostBuild t m => PostBuild t (ThemeReader t m)
491+deriving instance TriggerEvent t m => TriggerEvent t (ThemeReader t m)
492+instance HasImageWriter t m => HasImageWriter t (ThemeReader t m)
493+
494+instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (ThemeReader t m) where
495+ runWithReplace (ThemeReader a) e = ThemeReader $ runWithReplace a $ fmap unThemeReader e
496+ traverseIntMapWithKeyWithAdjust f m e = ThemeReader $ traverseIntMapWithKeyWithAdjust (\k v -> unThemeReader $ f k v) m e
497+ traverseDMapWithKeyWithAdjust f m e = ThemeReader $ traverseDMapWithKeyWithAdjust (\k v -> unThemeReader $ f k v) m e
498+ traverseDMapWithKeyWithAdjustWithMove f m e = ThemeReader $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unThemeReader $ f k v) m e
499+
500+instance MonadTrans (ThemeReader t) where
501+ lift = ThemeReader . lift
717
502
718−-- | Renders text, wrapped to the container width
503+instance MFunctor (ThemeReader t) where
719−text
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
720 :: (Reflex t, Monad m)
510 :: (Reflex t, Monad m)
721− => Behavior t Text
511+ => Behavior t V.Attr
722− -> VtyWidget t m ()
512+ -> ThemeReader t m a
723−text = richText def
513+ -> m a
724−
514+runThemeReader b = flip runReaderT b . unThemeReader
725−-- | Scrollable text widget. The output pair exposes the current scroll position and total number of lines (including those
515+
726−-- that are hidden)
516+
727−scrollableText
517+-- ** Manipulating images
728− :: forall t m. (Reflex t, MonadHold t m, MonadFix m)
518+
729− => Event t Int
519+-- | Translates and crops an 'Image' so that it is contained by
730− -- ^ Number of lines to scroll by
520+-- the given 'Region'.
731− -> Behavior t Text
521+withinImage
732− -> VtyWidget t m (Behavior t (Int, Int))
522+ :: Region
733− -- ^ (Current scroll position, total number of lines)
523+ -> Image
734−scrollableText scrollBy t = do
524+ -> Image
735− dw <- displayWidth
525+withinImage (Region left top width height)
736− let imgs = wrap <$> current dw <*> t
526+ | width < 0 || height < 0 = withinImage (Region left top 0 0)
737− kup <- key V.KUp
527+ | otherwise = V.translate left top . V.crop width height
738− kdown <- key V.KDown
528+
739− m <- mouseScroll
529+-- | Crop a behavior of images to a behavior of regions. See 'withinImage'.
740− let requestedScroll :: Event t Int
530+imagesInRegion
741− requestedScroll = leftmost
531+ :: Reflex t
742− [ 1 <$ kdown
532+ => Behavior t Region
743− , (-1) <$ kup
533+ -> Behavior t [Image]
744− , ffor m $ \case
534+ -> Behavior t [Image]
745− ScrollDirection_Up -> (-1)
535+imagesInRegion reg = liftA2 (\r is -> map (withinImage r) is) reg
746− ScrollDirection_Down -> 1
536+
747− , scrollBy
537+-- * Running sub-widgets
748− ]
538+
749− updateLine maxN delta ix = min (max 0 (ix + delta)) maxN
539+-- | Low-level widget combinator that runs a child widget within
750− lineIndex :: Dynamic t Int <- foldDyn (\(maxN, delta) ix -> updateLine (maxN - 1) delta ix) 0 $
540+-- a given region and context. This widget filters and modifies the input
751− attach (length <$> imgs) requestedScroll
541+-- that the child widget receives such that:
752− tellImages $ fmap ((:[]) . V.vertCat) $ drop <$> current lineIndex <*> imgs
542+-- * unfocused widgets receive no key events
753− return $ (,) <$> ((+) <$> current lineIndex <*> pure 1) <*> (length <$> imgs)
543+-- * mouse inputs outside the region are ignored
754− where
544+-- * mouse inputs inside the region have their coordinates translated such
755− wrap maxWidth = concatMap (fmap (V.string V.defAttr . T.unpack) . TZ.wrapWithOffset maxWidth 0) . T.split (=='\n')
545+-- that (0,0) is the top-left corner of the region
546+pane
547+ :: (Reflex t, Monad m, HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
548+ => Dynamic t Region
549+ -> Dynamic t Bool -- ^ Whether the widget should be focused when the parent is.
550+ -> m a
551+ -> m a
552+pane dr foc child = localRegion (const dr) $
553+ mapImages (imagesInRegion $ current dr) $
554+ localFocus (const foc) $
555+ inputInFocusedRegion >>= \e -> localInput (const e) child
756
556
757−-- | Renders any behavior whose value can be converted to
557+-- * Misc
758−-- 'String' as text
759−display
760− :: (Reflex t, Monad m, Show a)
761− => Behavior t a
762− -> VtyWidget t m ()
763−display a = text $ T.pack . show <$> a
764
558
765-- | A widget that draws nothing
559-- | A widget that draws nothing
766−blank :: Monad m => VtyWidget t m ()
560+blank :: Monad m => m ()
767blank = return ()
561blank = return ()
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+40−22
@@ -2,24 +2,26 @@
2Module: Reflex.Vty.Widget.Input
2Module: Reflex.Vty.Widget.Input
3Description: User input widgets for reflex-vty
3Description: User input widgets for reflex-vty
4-}
4-}
5−{-# LANGUAGE LambdaCase #-}
6−{-# LANGUAGE OverloadedStrings #-}
7module Reflex.Vty.Widget.Input
5module Reflex.Vty.Widget.Input
8 ( module Export
6 ( module Export
9 , module Reflex.Vty.Widget.Input
7 , module Reflex.Vty.Widget.Input
10 ) where
8 ) where
11
9
12
10
11+import Reflex.Vty.Widget.Input.Mouse as Export
13import Reflex.Vty.Widget.Input.Text as Export
12import Reflex.Vty.Widget.Input.Text as Export
14
13
15import Control.Monad (join)
14import Control.Monad (join)
16import Control.Monad.Fix (MonadFix)
15import Control.Monad.Fix (MonadFix)
17−import Control.Monad.NodeId (MonadNodeId)
18import Data.Default (Default(..))
16import Data.Default (Default(..))
19import Data.Text (Text)
17import Data.Text (Text)
20import qualified Graphics.Vty as V
18import qualified Graphics.Vty as V
21import Reflex
19import Reflex
22import Reflex.Vty.Widget
20import Reflex.Vty.Widget
21+import Reflex.Vty.Widget.Box
22+import Reflex.Vty.Widget.Text
23+
24+-- * Buttons
23
25
24-- | Configuration options for the 'button' widget
26-- | Configuration options for the 'button' widget
25data ButtonConfig t = ButtonConfig
27data ButtonConfig t = ButtonConfig
@@ -32,10 +34,10 @@ instance Reflex t => Default (ButtonConfig t) where
32
34
33-- | A button widget that contains a sub-widget
35-- | A button widget that contains a sub-widget
34button
36button
35− :: (Reflex t, Monad m, MonadNodeId m)
37+ :: (Reflex t, Monad m, HasFocusReader t m, HasTheme t m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m)
36 => ButtonConfig t
38 => ButtonConfig t
37− -> VtyWidget t m ()
39+ -> m ()
38− -> VtyWidget t m (Event t ())
40+ -> m (Event t ())
39button cfg child = do
41button cfg child = do
40 f <- focus
42 f <- focus
41 let style = do
43 let style = do
@@ -50,39 +52,44 @@ button cfg child = do
50
52
51-- | A button widget that displays text that can change
53-- | A button widget that displays text that can change
52textButton
54textButton
53− :: (Reflex t, Monad m, MonadNodeId m)
55+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasFocusReader t m, HasTheme t m, HasImageWriter t m, HasInput t m)
54 => ButtonConfig t
56 => ButtonConfig t
55 -> Behavior t Text
57 -> Behavior t Text
56− -> VtyWidget t m (Event t ())
58+ -> m (Event t ())
57textButton cfg = button cfg . text -- TODO Centering etc.
59textButton cfg = button cfg . text -- TODO Centering etc.
58
60
59-- | A button widget that displays a static bit of text
61-- | A button widget that displays a static bit of text
60textButtonStatic
62textButtonStatic
61− :: (Reflex t, Monad m, MonadNodeId m)
63+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasFocusReader t m, HasTheme t m, HasImageWriter t m, HasInput t m)
62 => ButtonConfig t
64 => ButtonConfig t
63 -> Text
65 -> Text
64− -> VtyWidget t m (Event t ())
66+ -> m (Event t ())
65textButtonStatic cfg = textButton cfg . pure
67textButtonStatic cfg = textButton cfg . pure
66
68
69+-- * Links
70+
67-- | A clickable link widget
71-- | A clickable link widget
68link
72link
69− :: (Reflex t, Monad m)
73+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasTheme t m)
70 => Behavior t Text
74 => Behavior t Text
71− -> VtyWidget t m (Event t MouseUp)
75+ -> m (Event t MouseUp)
72link t = do
76link t = do
77+ bt <- theme
73 let cfg = RichTextConfig
78 let cfg = RichTextConfig
74− { _richTextConfig_attributes = pure $ V.withStyle V.defAttr V.underline
79+ { _richTextConfig_attributes = fmap (\attr -> V.withStyle attr V.underline) bt
75 }
80 }
76 richText cfg t
81 richText cfg t
77 mouseUp
82 mouseUp
78
83
79-- | A clickable link widget with a static label
84-- | A clickable link widget with a static label
80linkStatic
85linkStatic
81− :: (Reflex t, Monad m)
86+ :: (Reflex t, Monad m, HasImageWriter t m, HasDisplayRegion t m, HasInput t m, HasTheme t m)
82 => Text
87 => Text
83− -> VtyWidget t m (Event t MouseUp)
88+ -> m (Event t MouseUp)
84linkStatic = link . pure
89linkStatic = link . pure
85
90
91+-- * Checkboxes
92+
86-- | Characters used to render checked and unchecked textboxes
93-- | Characters used to render checked and unchecked textboxes
87data CheckboxStyle = CheckboxStyle
94data CheckboxStyle = CheckboxStyle
88 { _checkboxStyle_unchecked :: Text
95 { _checkboxStyle_unchecked :: Text
@@ -109,32 +116,43 @@ checkboxStyleTick = CheckboxStyle
109-- | Configuration options for a checkbox
116-- | Configuration options for a checkbox
110data CheckboxConfig t = CheckboxConfig
117data CheckboxConfig t = CheckboxConfig
111 { _checkboxConfig_checkboxStyle :: Behavior t CheckboxStyle
118 { _checkboxConfig_checkboxStyle :: Behavior t CheckboxStyle
119+ -- TODO DELETE and use HasTheme instead
112 , _checkboxConfig_attributes :: Behavior t V.Attr
120 , _checkboxConfig_attributes :: Behavior t V.Attr
121+ , _checkboxConfig_setValue :: Event t Bool
113 }
122 }
114
123
115instance (Reflex t) => Default (CheckboxConfig t) where
124instance (Reflex t) => Default (CheckboxConfig t) where
116 def = CheckboxConfig
125 def = CheckboxConfig
117 { _checkboxConfig_checkboxStyle = pure def
126 { _checkboxConfig_checkboxStyle = pure def
118 , _checkboxConfig_attributes = pure V.defAttr
127 , _checkboxConfig_attributes = pure V.defAttr
128+ , _checkboxConfig_setValue = never
119 }
129 }
120
130
121-- | A checkbox widget
131-- | A checkbox widget
122checkbox
132checkbox
123− :: (MonadHold t m, MonadFix m, Reflex t)
133+ :: (MonadHold t m, MonadFix m, Reflex t, HasInput t m, HasDisplayRegion t m, HasImageWriter t m, HasFocusReader t m, HasTheme t m)
124 => CheckboxConfig t
134 => CheckboxConfig t
125 -> Bool
135 -> Bool
126− -> VtyWidget t m (Dynamic t Bool)
136+ -> m (Dynamic t Bool)
127checkbox cfg v0 = do
137checkbox cfg v0 = do
128 md <- mouseDown V.BLeft
138 md <- mouseDown V.BLeft
129 mu <- mouseUp
139 mu <- mouseUp
130− v <- toggle v0 $ () <$ mu
140+ space <- key (V.KChar ' ')
141+ f <- focus
142+ v <- foldDyn ($) v0 $ leftmost
143+ [ not <$ mu
144+ , not <$ space
145+ , const <$> _checkboxConfig_setValue cfg
146+ ]
147+ let bold = V.withStyle mempty V.bold
131 depressed <- hold mempty $ leftmost
148 depressed <- hold mempty $ leftmost
132− [ V.withStyle mempty V.bold <$ md
149+ [ bold <$ md
133 , mempty <$ mu
150 , mempty <$ mu
134 ]
151 ]
152+ let focused = ffor (current f) $ \x -> if x then bold else mempty
135− let attrs = (<>) <$> (_checkboxConfig_attributes cfg) <*> depressed
153+ let attrs = mconcat <$> sequence [_checkboxConfig_attributes cfg, depressed, focused]
136 richText (RichTextConfig attrs) $ join . current $ ffor v $ \checked ->
154 richText (RichTextConfig attrs) $ join . current $ ffor v $ \checked ->
137 if checked
155 if checked
138− then fmap _checkboxStyle_checked $ _checkboxConfig_checkboxStyle cfg
156+ then _checkboxStyle_checked <$> _checkboxConfig_checkboxStyle cfg
139− else fmap _checkboxStyle_unchecked $ _checkboxConfig_checkboxStyle cfg
157+ else _checkboxStyle_unchecked <$> _checkboxConfig_checkboxStyle cfg
140 return v
158 return v
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+30−31
@@ -2,11 +2,6 @@
2Module: Reflex.Vty.Widget.Input.Text
2Module: Reflex.Vty.Widget.Input.Text
3Description: Widgets for accepting text input from users and manipulating text within those inputs
3Description: Widgets for accepting text input from users and manipulating text within those inputs
4-}
4-}
5−{-# LANGUAGE FlexibleContexts #-}
6−{-# LANGUAGE LambdaCase #-}
7−{-# LANGUAGE OverloadedStrings #-}
8−{-# LANGUAGE RecursiveDo #-}
9−{-# LANGUAGE ScopedTypeVariables #-}
10module Reflex.Vty.Widget.Input.Text
5module Reflex.Vty.Widget.Input.Text
11 ( module Reflex.Vty.Widget.Input.Text
6 ( module Reflex.Vty.Widget.Input.Text
12 , def
7 , def
@@ -14,7 +9,6 @@ module Reflex.Vty.Widget.Input.Text
14
9
15import Control.Monad (join)
10import Control.Monad (join)
16import Control.Monad.Fix (MonadFix)
11import Control.Monad.Fix (MonadFix)
17−import Control.Monad.NodeId (MonadNodeId)
18import Data.Default (Default(..))
12import Data.Default (Default(..))
19import Data.Text (Text)
13import Data.Text (Text)
20import Data.Text.Zipper
14import Data.Text.Zipper
@@ -23,6 +17,7 @@ import Reflex
23
17
24import Reflex.Vty.Widget
18import Reflex.Vty.Widget
25import Reflex.Vty.Widget.Layout
19import Reflex.Vty.Widget.Layout
20+import Reflex.Vty.Widget.Input.Mouse
26
21
27-- | Configuration options for a 'textInput'. For more information on
22-- | Configuration options for a 'textInput'. For more information on
28-- 'TextZipper', see 'Data.Text.Zipper'.
23-- 'TextZipper', see 'Data.Text.Zipper'.
@@ -48,14 +43,16 @@ data TextInput t = TextInput
48
43
49-- | A widget that allows text input
44-- | A widget that allows text input
50textInput
45textInput
51− :: (Reflex t, MonadHold t m, MonadFix 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)
52 => TextInputConfig t
47 => TextInputConfig t
53− -> VtyWidget t m (TextInput t)
48+ -> m (TextInput t)
54textInput cfg = do
49textInput cfg = do
55 i <- input
50 i <- input
56 f <- focus
51 f <- focus
57 dh <- displayHeight
52 dh <- displayHeight
58 dw <- displayWidth
53 dw <- displayWidth
54+ bt <- theme
55+ attr0 <- sample bt
59 rec v <- foldDyn ($) (_textInputConfig_initialValue cfg) $ mergeWith (.)
56 rec v <- foldDyn ($) (_textInputConfig_initialValue cfg) $ mergeWith (.)
60 [ uncurry (updateTextZipper (_textInputConfig_tabWidth cfg)) <$> attach (current dh) i
57 [ uncurry (updateTextZipper (_textInputConfig_tabWidth cfg)) <$> attach (current dh) i
61 , _textInputConfig_modify cfg
58 , _textInputConfig_modify cfg
@@ -64,13 +61,20 @@ textInput cfg = do
64 goToDisplayLinePosition mx (st + my) dl
61 goToDisplayLinePosition mx (st + my) dl
65 ]
62 ]
66 click <- mouseDown V.BLeft
63 click <- mouseDown V.BLeft
67− let cursorAttrs = ffor f $ \x -> if x then cursorAttributes else V.defAttr
64+
68− let rows = (\w s c -> displayLines w V.defAttr c s)
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 = (,,)
69 <$> dw
68 <$> dw
70 <*> (mapZipper <$> _textInputConfig_display cfg <*> v)
69 <*> (mapZipper <$> _textInputConfig_display cfg <*> v)
71− <*> 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
72 img = images . _displayLines_spans <$> rows
76 img = images . _displayLines_spans <$> rows
73− y <- holdUniqDyn $ _displayLines_cursorY <$> rows
77+ y <- holdUniqDyn $ fmap snd _displayLines_cursorPos <$> rows
74 let newScrollTop :: Int -> (Int, Int) -> Int
78 let newScrollTop :: Int -> (Int, Int) -> Int
75 newScrollTop st (h, cursorY)
79 newScrollTop st (h, cursorY)
76 | cursorY < st = cursorY
80 | cursorY < st = cursorY
@@ -86,9 +90,9 @@ textInput cfg = do
86
90
87-- | A widget that allows multiline text input
91-- | A widget that allows multiline text input
88multilineTextInput
92multilineTextInput
89− :: (Reflex t, MonadHold t m, MonadFix m)
93+ :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m, HasFocusReader t m, HasTheme t m, HasDisplayRegion t m, HasImageWriter t m)
90 => TextInputConfig t
94 => TextInputConfig t
91− -> VtyWidget t m (TextInput t)
95+ -> m (TextInput t)
92multilineTextInput cfg = do
96multilineTextInput cfg = do
93 i <- input
97 i <- input
94 textInput $ cfg
98 textInput $ cfg
@@ -103,23 +107,18 @@ multilineTextInput cfg = do
103-- | Wraps a 'textInput' or 'multilineTextInput' in a tile. Uses
107-- | Wraps a 'textInput' or 'multilineTextInput' in a tile. Uses
104-- the computed line count to greedily size the tile when vertically
108-- the computed line count to greedily size the tile when vertically
105-- oriented, and uses the fallback width when horizontally oriented.
109-- oriented, and uses the fallback width when horizontally oriented.
106−-- TODO
107−-- textInputTile
110+textInputTile
108−-- :: (Reflex t, MonadHold t m, MonadFix m, MonadNodeId 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)
109−-- => VtyWidget t m (TextInput t)
112+ => m (TextInput t)
110−-- -> Dynamic t Int
113+ -> Dynamic t Int
111−-- -> Layout t m (TextInput t)
114+ -> m (TextInput t)
112−-- textInputTile txt width = do
115+textInputTile txt width = do
113−-- o <- askOrientation
116+ o <- askOrientation
114−-- rec t <- fixed sz txt
117+ rec t <- tile (Constraint_Fixed <$> sz) txt
115−-- let sz = join $ ffor o $ \case
118+ let sz = join $ ffor o $ \case
116−-- Orientation_Column -> _textInput_lines t
119+ Orientation_Column -> _textInput_lines t
117−-- Orientation_Row -> width
120+ Orientation_Row -> width
118−-- return t
121+ return t
119−
120−-- | Default attributes for the text cursor
121−cursorAttributes :: V.Attr
122−cursorAttributes = V.withStyle V.defAttr V.reverseVideo
123
122
124-- | Turn a set of display line rows into a list of images (one per line)
123-- | Turn a set of display line rows into a list of images (one per line)
125images :: [[Span V.Attr]] -> [V.Image]
124images :: [[Span V.Attr]] -> [V.Image]
src/Reflex/Vty/Widget/Layout.hs+441−439
@@ -2,58 +2,98 @@
2Module: Reflex.Vty.Widget.Layout
2Module: Reflex.Vty.Widget.Layout
3Description: Monad transformer and tools for arranging widgets and building screen layouts
3Description: Monad transformer and tools for arranging widgets and building screen layouts
4-}
4-}
5−{-# LANGUAGE FlexibleInstances #-}
6−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
7−{-# LANGUAGE LambdaCase #-}
8−{-# LANGUAGE MultiParamTypeClasses #-}
9−{-# LANGUAGE RecursiveDo #-}
10−{-# LANGUAGE ScopedTypeVariables #-}
11−{-# LANGUAGE OverloadedStrings #-}
12−{-# LANGUAGE TupleSections #-}
13−{-# LANGUAGE UndecidableInstances #-}
5+{-# Language UndecidableInstances #-}
14
6
15module Reflex.Vty.Widget.Layout where
7module Reflex.Vty.Widget.Layout where
16− -- ( Orientation(..)
8+
17− -- , Constraint(..)
9+import Control.Applicative (liftA2)
18− -- , Layout
10+import Control.Monad.Morph
19− -- , runLayout
20− -- , TileConfig(..)
21− -- , tile
22− -- , fixed
23− -- , stretch
24− -- , col
25− -- , row
26− -- , tabNavigation
27− -- , askOrientation
28− -- ) where
29−
30−import Control.Monad.NodeId (MonadNodeId(..), NodeId, NodeIdT)
11+import Control.Monad.NodeId (MonadNodeId(..), NodeId)
31import Control.Monad.Reader
12import Control.Monad.Reader
32−import Data.Bimap (Bimap)
13+import Data.List (mapAccumL)
33−import qualified Data.Bimap as Bimap
34−import Data.Default (Default(..))
35−import Data.List
36−import Data.Map (Map)
14+import Data.Map.Ordered (OMap)
37−import qualified Data.Map as Map
15+import qualified Data.Map.Ordered as OMap
38−import Data.Maybe (fromMaybe)
16+import Data.Maybe (fromMaybe, isNothing)
39−import Data.Monoid hiding (First(..))
40import Data.Ratio ((%))
17import Data.Ratio ((%))
41import Data.Semigroup (First(..))
18import Data.Semigroup (First(..))
42−import Data.Sequence (Seq)
19+import Data.Set.Ordered (OSet)
43−import qualified Data.Sequence as Seq
20+import qualified Data.Set.Ordered as OSet
44import qualified Graphics.Vty as V
21import qualified Graphics.Vty as V
45−import Data.Map.Ordered (OMap, Bias(..), L)
46−import qualified Data.Map.Ordered as OMap
47−import qualified Graphics.Vty.Attributes as V
48
22
49import Reflex
23import Reflex
50import Reflex.Host.Class (MonadReflexCreateTrigger)
24import Reflex.Host.Class (MonadReflexCreateTrigger)
51import Reflex.Vty.Widget
25import Reflex.Vty.Widget
26+import Reflex.Vty.Widget.Input.Mouse
52
27
53-- * Focus
28-- * Focus
29+--
30+-- $focus
31+--
32+-- The focus monad tracks which element is currently focused and processes
33+-- requests to change focus. Focusable elements are assigned a 'FocusId' and
34+-- can manually request focus or receive focus due to some other action (e.g.,
35+-- a tab press in a sibling element, a click event).
36+--
37+-- Focusable elements will usually be created via 'tile', but can also be
38+-- constructed via 'makeFocus' in 'HasFocus'. The latter option allows for
39+-- more find-grained control of focus behavior.
40+
41+-- ** Storing focus state
42+
43+-- | Identifies an element that is focusable. Can be created using 'makeFocus'.
44+newtype FocusId = FocusId NodeId
45+ deriving (Eq, Ord)
54
46
47+-- | An ordered set of focus identifiers. The order here determines the order
48+-- in which focus cycles between focusable elements.
49+newtype FocusSet = FocusSet { unFocusSet :: OSet FocusId }
50+
51+instance Semigroup FocusSet where
52+ FocusSet a <> FocusSet b = FocusSet $ a OSet.|<> b
53+
54+instance Monoid FocusSet where
55+ mempty = FocusSet OSet.empty
56+
57+-- | Produces a 'FocusSet' with a single element
58+singletonFS :: FocusId -> FocusSet
59+singletonFS = FocusSet . OSet.singleton
60+
61+-- ** Changing focus state
62+
63+-- | Operations that change the currently focused element.
64+data Refocus = Refocus_Shift Int -- ^ Shift the focus by a certain number of positions (see 'shiftFS')
65+ | Refocus_Id FocusId -- ^ Focus a particular element
66+ | Refocus_Clear -- ^ Remove focus from all elements
67+
68+-- | Given a 'FocusSet', a currently focused element, and a number of positions
69+-- to move by, determine the newly focused element.
70+shiftFS :: FocusSet -> Maybe FocusId -> Int -> Maybe FocusId
71+shiftFS (FocusSet s) fid n = case OSet.findIndex <$> fid <*> pure s of
72+ Nothing -> OSet.elemAt s 0
73+ Just Nothing -> OSet.elemAt s 0
74+ Just (Just ix) -> OSet.elemAt s $ mod (ix + n) (OSet.size s)
75+
76+-- ** The focus management monad
77+
78+-- | A class for things that can produce focusable elements.
79+class (Monad m, Reflex t) => HasFocus t m | m -> t where
80+ -- | Create a focusable element.
81+ makeFocus :: m FocusId
82+ -- | Emit an 'Event' of requests to change the focus.
83+ requestFocus :: Event t Refocus -> m ()
84+ -- | Produce a 'Dynamic' that indicates whether the given 'FocusId' is focused.
85+ isFocused :: FocusId -> m (Dynamic t Bool)
86+ -- | Run an action, additionally returning the focusable elements it produced.
87+ subFoci :: m a -> m (a, Dynamic t FocusSet)
88+ -- | Get a 'Dynamic' of the currently focused element identifier.
89+ focusedId :: m (Dynamic t (Maybe FocusId))
90+
91+-- | A monad transformer that keeps track of the set of focusable elements and
92+-- which, if any, are currently focused, and allows focus requests.
55newtype Focus t m a = Focus
93newtype Focus t m a = Focus
56− { unFocus :: DynamicWriterT t (Seq FocusId) (ReaderT (Demux t (Maybe FocusId)) m) a
94+ { unFocus :: DynamicWriterT t FocusSet
95+ (ReaderT (Dynamic t (Maybe FocusId))
96+ (EventWriterT t (First Refocus) m)) a
57 }
97 }
58 deriving
98 deriving
59 ( Functor
99 ( Functor
@@ -66,194 +106,248 @@ newtype Focus t m a = Focus
66 , PerformEvent t
106 , PerformEvent t
67 , NotReady t
107 , NotReady t
68 , MonadReflexCreateTrigger t
108 , MonadReflexCreateTrigger t
69− , HasDisplaySize t
109+ , HasDisplayRegion t
70 , PostBuild t
110 , PostBuild t
71 , MonadNodeId
111 , MonadNodeId
112+ , MonadIO
72 )
113 )
73
114
74−instance MonadTrans (Focus t) where
75− lift = Focus . lift . lift
76−
77−newtype FocusId = FocusId NodeId
78− deriving (Eq, Ord)
79−
80−focusId :: (MonadNodeId m, Reflex t) => Focus t m FocusId
81−focusId = do
82− fid <- FocusId <$> lift getNextNodeId
83− Focus $ tellDyn $ pure $ Seq.singleton fid
84− pure fid
85
115
116+instance MonadTrans (Focus t) where
117+ lift = Focus . lift . lift . lift
118+
119+instance MFunctor (Focus t) where
120+ hoist f = Focus . hoist (hoist (hoist f)) . unFocus
121+
122+instance (Adjustable t m, MonadHold t m, MonadFix m) => Adjustable t (Focus t m) where
123+ runWithReplace (Focus a) e = Focus $ runWithReplace a $ fmap unFocus e
124+ traverseIntMapWithKeyWithAdjust f m e = Focus $ traverseIntMapWithKeyWithAdjust (\k v -> unFocus $ f k v) m e
125+ traverseDMapWithKeyWithAdjust f m e = Focus $ traverseDMapWithKeyWithAdjust (\k v -> unFocus $ f k v) m e
126+ traverseDMapWithKeyWithAdjustWithMove f m e = Focus $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unFocus $ f k v) m e
127+
128+instance (Reflex t, MonadFix m, HasInput t m) => HasInput t (Focus t m) where
129+ localInput f = hoist (localInput f)
130+
131+instance (HasImageWriter t m, MonadFix m) => HasImageWriter t (Focus t m) where
132+ mapImages f = hoist (mapImages f)
133+
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)
137+
138+instance (Reflex t, MonadFix m, MonadNodeId m) => HasFocus t (Focus t m) where
139+ makeFocus = do
140+ fid <- FocusId <$> lift getNextNodeId
141+ Focus $ tellDyn $ pure $ singletonFS fid
142+ pure fid
143+ requestFocus = Focus . tellEvent . fmap First
144+ isFocused fid = do
145+ sel <- Focus ask
146+ pure $ (== Just fid) <$> sel
147+ subFoci (Focus child) = Focus $ do
148+ (a, fs) <- lift $ runDynamicWriterT child
149+ tellDyn fs
150+ return (a, fs)
151+ focusedId = Focus ask
152+
153+-- | Runs a 'Focus' action, maintaining the selection state internally.
86runFocus
154runFocus
87 :: (MonadFix m, MonadHold t m, Reflex t)
155 :: (MonadFix m, MonadHold t m, Reflex t)
88− => Event t (Maybe FocusId)
89− -> Focus t m a
156+ => Focus t m a
90− -> m (a, Dynamic t (Seq FocusId))
157+ -> m (a, Dynamic t FocusSet)
91−runFocus e (Focus x) = do
158+runFocus (Focus x) = do
92− rec (a, focusIds) <- flip runReaderT (demux sel) $ runDynamicWriterT x
159+ rec ((a, focusIds), focusRequests) <- runEventWriterT $ flip runReaderT sel $ runDynamicWriterT x
93− sel <- holdDyn Nothing e
160+ sel <- foldDyn f Nothing $ attach (current focusIds) focusRequests
94 pure (a, focusIds)
161 pure (a, focusIds)
95−
162+ where
96−withFocus
163+ f :: (FocusSet, First Refocus) -> Maybe FocusId -> Maybe FocusId
97− :: (Reflex t, Monad m)
164+ f (fs, rf) mf = case getFirst rf of
98− => FocusId
165+ Refocus_Clear -> Nothing
99− -> (Dynamic t Bool -> Focus t m a) -- TODO unnecessary: just return the dynamic bool
166+ Refocus_Id fid -> Just fid
100− -> Focus t m a
167+ Refocus_Shift n -> if n < 0 && isNothing mf
101−withFocus focusId builder = do
168+ then shiftFS fs (OSet.elemAt (unFocusSet fs) 0) n
102− sel <- Focus ask
169+ else shiftFS fs mf n
103− builder $ demuxed sel $ Just focusId
170+
104−
171+-- | Runs an action in the focus monad, providing it with information about
105−-- focus builder = do
172+-- whether any of the foci created within it are focused.
106−-- fid <- focusId
173+anyChildFocused
107−-- withFocus fid builder
108−
109−-- | Configuration options for and constraints on 'tile'
110−data TileConfig t = TileConfig
111− { _tileConfig_constraint :: Dynamic t Constraint
112− -- ^ 'Constraint' on the tile's size
113− , _tileConfig_focusable :: Dynamic t Bool
114− -- ^ Whether the tile is focusable
115− }
116−
117−instance Reflex t => Default (TileConfig t) where
118− def = TileConfig (pure $ Constraint_Min 0) (pure True)
119−
120−tile
121− :: (MonadNodeId m, MonadFix m, Reflex t)
174+ :: (HasFocus t m, MonadFix m)
122− => VtyWidget t m a
175+ => (Dynamic t Bool -> m a)
123− -> Layout t (Focus t (VtyWidget t m)) a
176+ -> m a
124−tile w = do
177+anyChildFocused f = do
125− fid <- lift focusId
178+ fid <- focusedId
126− focused <- lift $ withFocus fid return
179+ rec (a, fs) <- subFoci (f b)
127− stretch $ \r -> lift $ pane r focused w
180+ let b = liftA2 (\foc s -> case foc of
181+ Nothing -> False
182+ Just f' -> OSet.member f' $ unFocusSet s) fid fs
183+ pure a
184+
185+-- ** Focus controls
186+
187+-- | Request focus be shifted backward and forward based on tab presses. <Tab>
188+-- shifts focus forward and <Shift+Tab> shifts focus backward.
189+tabNavigation :: (Reflex t, HasInput t m, HasFocus t m) => m ()
190+tabNavigation = do
191+ fwd <- fmap (const 1) <$> key (V.KChar '\t')
192+ back <- fmap (const (-1)) <$> key V.KBackTab
193+ requestFocus $ Refocus_Shift <$> leftmost [fwd, back]
128
194
129-- * Layout
195-- * Layout
196+--
197+-- $layout
198+-- The layout monad keeps track of a tree of elements, each having its own
199+-- layout constraints and orientation. Given the available rendering space, it
200+-- computes a layout solution and provides child elements with their particular
201+-- layout solution (the width and height of their rendering space).
202+--
203+-- Complex layouts are built up though some combination of:
204+--
205+-- - 'axis', which lays out its children in a particular orientation, and
206+-- - 'region', which "claims" some part of the screen according to its constraints
207+--
208+
209+-- ** Layout restrictions
210+
211+-- *** Constraints
130
212
131−data LTree a = LTree a (LForest a)
213+-- | Datatype representing constraints on a widget's size along the main axis (see 'Orientation')
132−
214+data Constraint = Constraint_Fixed Int
133−newtype LForest a = LForest { unLForest :: OMap NodeId (LTree a) }
215+ | Constraint_Min Int
216+ deriving (Show, Read, Eq, Ord)
134
217
135−instance Semigroup (LForest a) where
218+-- | Shorthand for constructing a fixed constraint
136− LForest a <> LForest b = LForest $ a OMap.|<> b
219+fixed
220+ :: Reflex t
221+ => Dynamic t Int
222+ -> Dynamic t Constraint
223+fixed = fmap Constraint_Fixed
137
224
138−instance Monoid (LForest a) where
225+-- | Shorthand for constructing a minimum size constraint
139− mempty = LForest OMap.empty
226+stretch
227+ :: Reflex t
228+ => Dynamic t Int
229+ -> Dynamic t Constraint
230+stretch = fmap Constraint_Min
140
231
141−lookupLF :: NodeId -> LForest a -> Maybe (LTree a)
232+-- | Shorthand for constructing a constraint of no minimum size
142−lookupLF n (LForest a) = OMap.lookup n a
233+flex
234+ :: Reflex t
235+ => Dynamic t Constraint
236+flex = pure $ Constraint_Min 0
143
237
144−singletonLF :: NodeId -> LTree a -> LForest a
238+-- *** Orientation
145−singletonLF n t = LForest $ OMap.singleton (n, t)
146
239
147−fromListLF :: [(NodeId, LTree a)] -> LForest a
240+-- | The main-axis orientation of a 'Layout' widget
148−fromListLF = LForest . OMap.fromList
241+data Orientation = Orientation_Column
242+ | Orientation_Row
243+ deriving (Show, Read, Eq, Ord)
149
244
150−ltreeRoot :: LTree a -> a
245+-- | Create a row-oriented 'axis'
151−ltreeRoot (LTree a _) = a
246+row
247+ :: (Reflex t, MonadFix m, HasLayout t m)
248+ => m a
249+ -> m a
250+row = axis (pure Orientation_Row) flex
152
251
153−ltreeForest :: LTree a -> LForest a
252+-- | Create a column-oriented 'axis'
154−ltreeForest (LTree _ a) = a
253+col
254+ :: (Reflex t, MonadFix m, HasLayout t m)
255+ => m a
256+ -> m a
257+col = axis (pure Orientation_Column) flex
155
258
156−newtype Layout t m a = Layout
259+-- ** Layout management data
157− { unLayout :: DynamicWriterT t (LForest (Constraint, Orientation))
158− (ReaderT (Dynamic t (LTree (Region, Orientation))) m) a
159− }
160− deriving
161− ( Functor
162− , Applicative
163− , Monad
164− , MonadHold t
165− , MonadSample t
166− , MonadFix
167− , TriggerEvent t
168− , PerformEvent t
169− , NotReady t
170− , MonadReflexCreateTrigger t
171− , HasDisplaySize t
172− , PostBuild t
173− , MonadNodeId
174− )
175
260
176−instance MonadTrans (Layout t) where
261+-- | A collection of information related to the layout of the screen. The root
177− lift = Layout . lift . lift
262+-- node is a "parent" widget, and the contents of the 'LayoutForest' are its
263+-- children.
264+data LayoutTree a = LayoutTree a (LayoutForest a)
265+ deriving (Show)
178
266
179−-- | Datatype representing constraints on a widget's size along the main axis (see 'Orientation')
267+-- | An ordered, indexed collection of 'LayoutTree's representing information
180−data Constraint = Constraint_Fixed Int
268+-- about the children of some widget.
181− | Constraint_Min Int
269+newtype LayoutForest a = LayoutForest { unLayoutForest :: OMap NodeId (LayoutTree a) }
182− deriving (Show, Read, Eq, Ord)
270+ deriving (Show)
183
271
184−-- | The main-axis orientation of a 'Layout' widget
272+instance Semigroup (LayoutForest a) where
185−data Orientation = Orientation_Column
273+ LayoutForest a <> LayoutForest b = LayoutForest $ a OMap.|<> b
186− | Orientation_Row
187− deriving (Show, Read, Eq, Ord)
188
274
189−axis
275+instance Monoid (LayoutForest a) where
190− :: (Monad m, MonadNodeId m, Reflex t, MonadFix m)
276+ mempty = LayoutForest OMap.empty
191− => Orientation
192− -> Constraint
193− -> Layout t m a
194− -> Layout t m a
195−axis o c (Layout x) = Layout $ do
196− nodeId <- getNextNodeId
197− (result, forest) <- lift $ local (\t -> (\(Just x) -> x) . lookupLF nodeId . ltreeForest <$> t) $ runDynamicWriterT x -- TODO?
198− tellDyn $ fmap (singletonLF nodeId . LTree (c, o)) forest
199− pure result
200−
201−row, col
202− :: (Monad m, MonadNodeId m, Reflex t, MonadFix m)
203− => Constraint
204− -> Layout t m a
205− -> Layout t m a
206−row = axis Orientation_Row
207−col = axis Orientation_Column
208
277
209−leaf
278+-- | Perform a lookup by 'NodeId' in a 'LayoutForest'
210− :: (Monad m, MonadNodeId m, Reflex t, MonadFix m)
279+lookupLF :: NodeId -> LayoutForest a -> Maybe (LayoutTree a)
211− => Dynamic t Constraint
280+lookupLF n (LayoutForest a) = OMap.lookup n a
212− -> (DynRegion t -> m a)
213− -> Layout t m a
214−leaf c f = do
215− nodeId <- lift getNextNodeId
216− Layout $ tellDyn $ ffor c $ \c' -> singletonLF nodeId $ LTree (c', Orientation_Row) mempty
217− solutions <- Layout ask
218− let r = maybe (Region 0 0 0 0) (fst . ltreeRoot) . lookupLF nodeId . ltreeForest <$> solutions -- TODO revisit this fromMaybe
219− lift $ f $ dynRegion r
220
281
221−fixed
282+-- | Create a 'LayoutForest' with one element
222− :: (Monad m, MonadNodeId m, Reflex t, MonadFix m)
283+singletonLF :: NodeId -> LayoutTree a -> LayoutForest a
223− => Dynamic t Int
284+singletonLF n t = LayoutForest $ OMap.singleton (n, t)
224− -> (DynRegion t -> m a)
225− -> Layout t m a
226−fixed = leaf . fmap Constraint_Fixed
227
285
228−stretch
286+-- | Produce a 'LayoutForest' from a list. The order of the list is preserved.
229− :: (Monad m, MonadNodeId m, Reflex t, MonadFix m)
287+fromListLF :: [(NodeId, LayoutTree a)] -> LayoutForest a
230− => (DynRegion t -> m a)
288+fromListLF = LayoutForest . OMap.fromList
231− -> Layout t m a
232−stretch = leaf (pure $ Constraint_Min 0)
233
289
234−askOrientation :: (Monad m, Reflex t) => Layout t m (Dynamic t Orientation)
290+-- | Extract the information at the root of a 'LayoutTree'
235−askOrientation = Layout $ asks $ fmap (snd . ltreeRoot)
291+rootLT :: LayoutTree a -> a
292+rootLT (LayoutTree a _) = a
236
293
237−ltreeConstraint :: LTree (Constraint, a) -> Constraint
294+-- | Extract the child nodes of a 'LayoutTree'
238−ltreeConstraint (LTree (c, _) _) = c
295+childrenLT :: LayoutTree a -> LayoutForest a
296+childrenLT (LayoutTree _ a) = a
239
297
298+-- | Produce a layout solution given a starting orientation, the overall screen
299+-- size, and a set of constraints.
240solve
300solve
241 :: Orientation
301 :: Orientation
242 -> Region
302 -> Region
243− -> LForest (Constraint, Orientation)
303+ -> LayoutForest (Constraint, Orientation)
244− -> LTree (Region, Orientation)
304+ -> LayoutTree (Region, Orientation)
245−solve o0 r0 (LForest cs) =
305+solve o0 r0 (LayoutForest cs) =
246− let a = map (\(x, t) -> ((x, t), ltreeConstraint t)) $ OMap.assocs cs
306+ let a = map (\(x, t@(LayoutTree (c, _) _)) -> ((x, t), c)) $ OMap.assocs cs
247 extent = case o0 of
307 extent = case o0 of
248 Orientation_Row -> _region_width r0
308 Orientation_Row -> _region_width r0
249 Orientation_Column -> _region_height r0
309 Orientation_Column -> _region_height r0
250 sizes = computeEdges $ computeSizes extent a
310 sizes = computeEdges $ computeSizes extent a
251 chunks = [ (nodeId, solve o1 r1 f)
311 chunks = [ (nodeId, solve o1 r1 f)
252− | ((nodeId, LTree (_, o1) f), sz) <- sizes
312+ | ((nodeId, LayoutTree (_, o1) f), sz) <- sizes
253 , let r1 = chunk o0 r0 sz
313 , let r1 = chunk o0 r0 sz
254 ]
314 ]
255− in LTree (r0, o0) $ fromListLF chunks
315+ in LayoutTree (r0, o0) $ fromListLF chunks
316+ where
317+ computeEdges :: [(a, Int)] -> [(a, (Int, Int))]
318+ computeEdges = ($ []) . fst . foldl (\(m, offset) (a, sz) ->
319+ (((a, (offset, sz)) :) . m, sz + offset)) (id, 0)
320+ computeSizes
321+ :: Int
322+ -> [(a, Constraint)]
323+ -> [(a, Int)]
324+ computeSizes available constraints =
325+ -- The minimum amount of space we need. Calculated by adding up all of
326+ -- the fixed size items and all the minimum sizes of stretchable items
327+ let minTotal = sum $ ffor constraints $ \case
328+ (_, Constraint_Fixed n) -> n
329+ (_, Constraint_Min n) -> n
330+ -- The leftover space is the area we can allow stretchable items to
331+ -- expand into
332+ leftover = max 0 (available - minTotal)
333+ -- The number of stretchable items that will try to share some of the
334+ -- leftover space
335+ numStretch = length $ filter (isMin . snd) constraints
336+ -- Space to allocate to the stretchable items (this is the same for all
337+ -- items and there may still be additional leftover space that will have
338+ -- to be unevenly distributed)
339+ szStretch = floor $ leftover % max numStretch 1
340+ -- Remainder of available space after even distribution. This extra space
341+ -- will be distributed to as many stretchable widgets as possible.
342+ adjustment = max 0 $ available - minTotal - szStretch * numStretch
343+ in snd $ mapAccumL (\adj (a, c) -> case c of
344+ Constraint_Fixed n -> (adj, (a, n))
345+ Constraint_Min n -> (max 0 (adj-1), (a, n + szStretch + signum adj))) adjustment constraints
346+ isMin (Constraint_Min _) = True
347+ isMin _ = False
256
348
349+-- | Produce a 'Region' given a starting orientation and region, and the offset
350+-- and main-axis size of the chunk.
257chunk :: Orientation -> Region -> (Int, Int) -> Region
351chunk :: Orientation -> Region -> (Int, Int) -> Region
258chunk o r (offset, sz) = case o of
352chunk o r (offset, sz) = case o of
259 Orientation_Column -> r
353 Orientation_Column -> r
@@ -265,79 +359,48 @@ chunk o r (offset, sz) = case o of
265 , _region_width = sz
359 , _region_width = sz
266 }
360 }
267
361
268−-- | Compute the size of each widget "@k@" based on the total set of 'Constraint's
362+-- ** The layout monad
269−computeSizes
363+
270− :: Int
364+-- | A class of operations for creating screen layouts.
271− -> [(a, Constraint)]
365+class Monad m => HasLayout t m | m -> t where
272− -> [(a, Int)]
366+ -- | Starts a parent element in the current layout with the given size
273−computeSizes available constraints =
367+ -- constraint, which lays out its children according to the provided
274− let minTotal = sum $ ffor constraints $ \case
368+ -- orientation.
275− (_, Constraint_Fixed n) -> n
369+ axis :: Dynamic t Orientation -> Dynamic t Constraint -> m a -> m a
276− (_, Constraint_Min n) -> n
370+ -- | Creates a child element in the current layout with the given size
277− leftover = max 0 (available - minTotal)
371+ -- constraint, returning the 'Region' that the child element is allocated.
278− numStretch = length $ filter (isMin . snd) constraints
372+ region :: Dynamic t Constraint -> m (Dynamic t Region)
279− szStretch = floor $ leftover % (max numStretch 1)
373+ -- | Returns the orientation of the containing 'axis'.
280− adjustment = max 0 $ available - minTotal - szStretch * numStretch
374+ askOrientation :: m (Dynamic t Orientation)
281− in snd $ mapAccumL (\adj (a, c) -> case c of
375+
282− Constraint_Fixed n -> (adj, (a, n))
376+-- | A monad transformer that collects layout constraints and provides a layout
283− Constraint_Min n -> (0, (a, n + szStretch + adj))) adjustment constraints
377+-- solution that satisfies those constraints.
284− where
285− isMin (Constraint_Min _) = True
286− isMin _ = False
287−
288−computeEdges :: [(a, Int)] -> [(a, (Int, Int))]
289−computeEdges = ($ []) . fst . foldl (\(m, offset) (a, sz) ->
290− (((a, (offset, sz)) :) . m, sz + offset)) (id, 0)
291−
292−runLayout
293− :: (MonadFix m, Reflex t)
294− => Dynamic t Orientation
295− -> Dynamic t Region
296− -> Layout t m a
297− -> m a
298−runLayout o r (Layout x) = do
299− rec (result, w) <- runReaderT (runDynamicWriterT x) solutions
300− let solutions = solve <$> o <*> r <*> w
301− return result
302−
303−{-
304−data LayoutSegment = LayoutSegment
305− { _layoutSegment_offset :: Int
306− , _layoutSegment_size :: Int
307− }
308−
309−data LayoutCtx t = LayoutCtx
310− { _layoutCtx_regions :: Dynamic t (Map NodeId LayoutSegment)
311− , _layoutCtx_focusDemux :: Demux t (Maybe NodeId)
312− , _layoutCtx_orientation :: Dynamic t Orientation
313− }
314−
315−-- | The Layout monad transformer keeps track of the configuration (e.g., 'Orientation') and
316−-- 'Constraint's of its child widgets, apportions vty real estate to each, and acts as a
317−-- switchboard for focus requests. See 'tile' and 'runLayout'.
318newtype Layout t m a = Layout
378newtype Layout t m a = Layout
319− { unLayout :: EventWriterT t (First NodeId)
320− (DynamicWriterT t (Endo [(NodeId, (Bool, Constraint))])
379+ { unLayout :: DynamicWriterT t (LayoutForest (Constraint, Orientation))
321− (ReaderT (LayoutCtx t)
380+ (ReaderT (Dynamic t (LayoutTree (Region, Orientation))) m) a
322− (VtyWidget t m))) a
381+ }
323− } deriving
382+ deriving
324 ( Functor
383 ( Functor
325 , Applicative
384 , Applicative
385+ , HasDisplayRegion t
326 , Monad
386 , Monad
387+ , MonadFix
327 , MonadHold t
388 , MonadHold t
389+ , MonadIO
390+ , MonadNodeId
391+ , MonadReflexCreateTrigger t
328 , MonadSample t
392 , MonadSample t
329− , MonadFix
330− , TriggerEvent t
331− , PerformEvent t
332 , NotReady t
393 , NotReady t
333− , MonadReflexCreateTrigger t
394+ , PerformEvent t
334− , HasDisplaySize t
335− , MonadNodeId
336 , PostBuild t
395 , PostBuild t
396+ , TriggerEvent t
337 )
397 )
338
398
339instance MonadTrans (Layout t) where
399instance MonadTrans (Layout t) where
340− lift x = Layout $ lift $ lift $ lift $ lift x
400+ lift = Layout . lift . lift
401+
402+instance MFunctor (Layout t) where
403+ hoist f = Layout . hoist (hoist f) . unLayout
341
404
342instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (Layout t m) where
405instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (Layout t m) where
343 runWithReplace (Layout a) e = Layout $ runWithReplace a $ fmap unLayout e
406 runWithReplace (Layout a) e = Layout $ runWithReplace a $ fmap unLayout e
@@ -345,203 +408,142 @@ instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (Layout t m
345 traverseDMapWithKeyWithAdjust f m e = Layout $ traverseDMapWithKeyWithAdjust (\k v -> unLayout $ f k v) m e
408 traverseDMapWithKeyWithAdjust f m e = Layout $ traverseDMapWithKeyWithAdjust (\k v -> unLayout $ f k v) m e
346 traverseDMapWithKeyWithAdjustWithMove f m e = Layout $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unLayout $ f k v) m e
409 traverseDMapWithKeyWithAdjustWithMove f m e = Layout $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unLayout $ f k v) m e
347
410
348−-- | Run a 'Layout' action
411+-- | Apply a transformation to the context of a child 'Layout' action and run
412+-- that action
413+hoistRunLayout
414+ :: (HasDisplayRegion t m, MonadFix m, Monad n)
415+ => (m a -> n b)
416+ -> Layout t m a
417+ -> Layout t n b
418+hoistRunLayout f x = do
419+ solution <- Layout ask
420+ let orientation = snd . rootLT <$> solution
421+ lift $ f $ do
422+ dw <- displayWidth
423+ dh <- displayHeight
424+ let reg = Region 0 0 <$> dw <*> dh
425+ runLayout orientation reg x
426+
427+instance (HasInput t m, HasDisplayRegion t m, MonadFix m, Reflex t) => HasInput t (Layout t m) where
428+ localInput = hoistRunLayout . localInput
429+
430+instance (HasDisplayRegion t m, HasImageWriter t m, MonadFix m) => HasImageWriter t (Layout t m) where
431+ mapImages f = hoistRunLayout (mapImages f)
432+
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)
436+
437+instance (Monad m, MonadNodeId m, Reflex t, MonadFix m) => HasLayout t (Layout t m) where
438+ axis o c (Layout x) = Layout $ do
439+ nodeId <- getNextNodeId
440+ let dummyParentLayout = LayoutTree (nilRegion, Orientation_Column) mempty
441+ (result, forest) <- lift $ local (\t -> fromMaybe dummyParentLayout . lookupLF nodeId . childrenLT <$> t) $ runDynamicWriterT x
442+ tellDyn $ singletonLF nodeId <$> (LayoutTree <$> ((,) <$> c <*> o) <*> forest)
443+ pure result
444+ region c = do
445+ nodeId <- lift getNextNodeId
446+ Layout $ tellDyn $ ffor c $ \c' -> singletonLF nodeId $ LayoutTree (c', Orientation_Row) mempty
447+ solutions <- Layout ask
448+ pure $ maybe nilRegion (fst . rootLT) . lookupLF nodeId . childrenLT <$> solutions
449+ askOrientation = Layout $ asks $ fmap (snd . rootLT)
450+
451+instance (MonadFix m, HasFocus t m) => HasFocus t (Layout t m) where
452+ makeFocus = lift makeFocus
453+ requestFocus = lift . requestFocus
454+ isFocused = lift . isFocused
455+ focusedId = lift focusedId
456+ subFoci (Layout x) = Layout $ do
457+ y <- ask
458+ ((a, w), sf) <- lift $ lift $ subFoci $ flip runReaderT y $ runDynamicWriterT x
459+ tellDyn w
460+ pure (a, sf)
461+
462+-- | Runs a 'Layout' action, using the given orientation and region to
463+-- calculate layout solutions.
349runLayout
464runLayout
350− :: (MonadFix m, MonadHold t m, PostBuild t m, Monad m, MonadNodeId m)
465+ :: (MonadFix m, Reflex t)
351− => Dynamic t Orientation -- ^ The main-axis 'Orientation' of this 'Layout'
466+ => Dynamic t Orientation
352− -> Int -- ^ The positional index of the initially focused tile
467+ -> Dynamic t Region
353− -> Event t Int -- ^ An event that shifts focus by a given number of tiles
354− -> Layout t m a -- ^ The 'Layout' widget
355− -> VtyWidget t m a
356−runLayout ddir focus0 focusShift (Layout child) = do
357− dw <- displayWidth
358− dh <- displayHeight
359− let main = ffor3 ddir dw dh $ \d w h -> case d of
360− Orientation_Column -> h
361− Orientation_Row -> w
362− pb <- getPostBuild
363− rec ((a, focusReq), queriesEndo) <- runReaderT (runDynamicWriterT $ runEventWriterT child) $ LayoutCtx solutionMap focusDemux ddir
364− let queries = flip appEndo [] <$> queriesEndo
365− solution = ffor2 main queries $ \sz qs -> Map.fromList
366− . Map.elems
367− . computeEdges
368− . computeSizes sz
369− . fmap (fmap snd)
370− . Map.fromList
371− . zip [0::Integer ..]
372− $ qs
373− solutionMap = ffor solution $ \ss -> ffor ss $ \(offset, sz) -> LayoutSegment
374− { _layoutSegment_offset = offset
375− , _layoutSegment_size = sz
376− }
377− -- focusable = fmap (Bimap.fromList . zip [0..]) $
378− -- ffor queries $ \qs -> fforMaybe qs $ \(nodeId, (f, _)) ->
379− -- if f then Just nodeId else Nothing
380− -- adjustFocus
381− -- :: (Bimap Int NodeId, (Int, Maybe NodeId))
382− -- -> Either Int NodeId
383− -- -> (Int, Maybe NodeId)
384− -- adjustFocus (fm, (cur, _)) (Left shift) =
385− -- let ix = (cur + shift) `mod` (max 1 $ Bimap.size fm)
386− -- in (ix, Bimap.lookup ix fm)
387− -- adjustFocus (fm, (cur, _)) (Right goto) =
388− -- let ix = fromMaybe cur $ Bimap.lookupR goto fm
389− -- in (ix, Just goto)
390− -- focusChange = attachWith
391− -- adjustFocus
392− -- (current $ (,) <$> focusable <*> focussed)
393− -- $ leftmost [Left <$> focusShift, Left 0 <$ pb, Right . getFirst <$> focusReq]
394− -- A pair (Int, Maybe NodeId) which represents the index
395− -- that we're trying to focus, and the node that actually gets
396− -- focused (at that index) if it exists
397− focussed <- holdDyn (focus0, Nothing) focusChange
398− let focusDemux = demux $ snd <$> focussed
399− return a
400−
401−-- | Tiles are the basic building blocks of 'Layout' widgets. Each tile has a constraint
402−-- on its size and ability to grow and on whether it can be focused. It also allows its child
403−-- widget to request focus.
404−tile
405− :: (Reflex t, Monad m, MonadNodeId m)
406− => TileConfig t -- ^ The tile's configuration
407− -> VtyWidget t m (Event t x, a) -- ^ A child widget. The 'Event' that it returns is used to request that it be focused.
408 -> Layout t m a
468 -> Layout t m a
409−tile (TileConfig con focusable) child = do
469+ -> m a
410− nodeId <- getNextNodeId
470+runLayout o r (Layout x) = do
411− Layout $ tellDyn $ ffor2 con focusable $ \c f -> Endo ((nodeId, (f, c)):)
471+ rec (result, w) <- runReaderT (runDynamicWriterT x) solutions
412− seg <- Layout $ asks $
472+ let solutions = solve <$> o <*> r <*> w
413− fmap (Map.findWithDefault (LayoutSegment 0 0) nodeId) . _layoutCtx_regions
473+ return result
474+
475+-- | Initialize and run the layout monad, using all of the available screen space.
476+initLayout :: (HasDisplayRegion t m, MonadFix m) => Layout t m a -> m a
477+initLayout f = do
414 dw <- displayWidth
478 dw <- displayWidth
415 dh <- displayHeight
479 dh <- displayHeight
416− o <- askOrientation
480+ let r = Region 0 0 <$> dw <*> dh
417− let cross = join $ ffor o $ \case
418− Orientation_Column -> dw
419− Orientation_Row -> dh
420− let reg = DynRegion
421− { _dynRegion_top = ffor2 seg o $ \s -> \case
422− Orientation_Column -> _layoutSegment_offset s
481+ runLayout (pure Orientation_Column) r f
423− Orientation_Row -> 0
482+
424− , _dynRegion_left = ffor2 seg o $ \s -> \case
483+-- * The tile "window manager"
425− Orientation_Column -> 0
484+--
426− Orientation_Row -> _layoutSegment_offset s
485+-- $tiling
427− , _dynRegion_width = ffor3 seg cross o $ \s c -> \case
486+-- Generally HasLayout and HasFocus are used together to build a user
428− Orientation_Column -> c
487+-- interface. These functions check the available screen size and initialize
429− Orientation_Row -> _layoutSegment_size s
488+-- the layout monad with that information, and also initialize the focus monad.
430− , _dynRegion_height = ffor3 seg cross o $ \s c -> \case
489+
431− Orientation_Column -> _layoutSegment_size s
490+-- | Initialize a 'Layout' and 'Focus' management context, returning the produced 'FocusSet'.
432− Orientation_Row -> c
491+initManager
433− }
492+ :: (HasDisplayRegion t m, Reflex t, MonadHold t m, MonadFix m)
434− focussed <- Layout $ asks _layoutCtx_focusDemux
493+ => Layout t (Focus t m) a
435− (focusReq, a) <- Layout $ lift $ lift $ lift $
494+ -> m (a, Dynamic t FocusSet)
436− pane reg (demuxed focussed $ Just nodeId) $ child
495+initManager =
437− Layout $ tellEvent $ First nodeId <$ focusReq
496+ runFocus . initLayout
438− return a
497+
439−
498+-- | Initialize a 'Layout' and 'Focus' management context.
440−-- | Configuration options for and constraints on 'tile'
499+initManager_
441−data TileConfig t = TileConfig
500+ :: (HasDisplayRegion t m, Reflex t, MonadHold t m, MonadFix m)
442− { _tileConfig_constraint :: Dynamic t Constraint
501+ => Layout t (Focus t m) a
443− -- ^ 'Constraint' on the tile's size
502+ -> m a
444− , _tileConfig_focusable :: Dynamic t Bool
503+initManager_ = fmap fst . initManager
445− -- ^ Whether the tile is focusable
446− }
447−
448−instance Reflex t => Default (TileConfig t) where
449− def = TileConfig (pure $ Constraint_Min 0) (pure True)
450−
451−-- | A 'tile' of a fixed size that is focusable and gains focus on click
452−fixed
453− :: (Reflex t, Monad m, MonadNodeId m)
454− => Dynamic t Int
455− -> VtyWidget t m a
456− -> Layout t m a
457−fixed sz = tile (def { _tileConfig_constraint = Constraint_Fixed <$> sz }) . clickable
458
504
459−-- | A 'tile' that can stretch (i.e., has no fixed size) and has a minimum size of 0.
460−-- This tile is focusable and gains focus on click.
461−stretch
462− :: (Reflex t, Monad m, MonadNodeId m)
463− => VtyWidget t m a
464− -> Layout t m a
505+-- ** Layout tiles
465−stretch = tile def . clickable
466
506
467−-- | A version of 'runLayout' that arranges tiles in a column and uses 'tabNavigation' to
507+-- *** Focusable
468−-- change tile focus.
469−col
470− :: (MonadFix m, MonadHold t m, PostBuild t m, MonadNodeId m)
471− => Layout t m a
472− -> VtyWidget t m a
473−col child = do
474− nav <- tabNavigation
475− runLayout (pure Orientation_Column) 0 nav child
476−
477−-- | A version of 'runLayout' that arranges tiles in a row and uses 'tabNavigation' to
478−-- change tile focus.
479−row
480− :: (MonadFix m, MonadHold t m, PostBuild t m, MonadNodeId m)
481− => Layout t m a
482− -> VtyWidget t m a
483−row child = do
484− nav <- tabNavigation
485− runLayout (pure Orientation_Row) 0 nav child
486−
487−-- | Produces an 'Event' that navigates forward one tile when the Tab key is pressed
488−-- and backward one tile when Shift+Tab is pressed.
489−tabNavigation :: (Reflex t, Monad m) => VtyWidget t m (Event t Int)
490−tabNavigation = do
491− fwd <- fmap (const 1) <$> key (V.KChar '\t')
492− back <- fmap (const (-1)) <$> key V.KBackTab
493− return $ leftmost [fwd, back]
494−
495−-- | Captures the click event in a 'VtyWidget' context and returns it. Useful for
496−-- requesting focus when using 'tile'.
497−clickable
498− :: (Reflex t, Monad m)
499− => VtyWidget t m a
500− -> VtyWidget t m (Event t (), a)
501−clickable child = do
502− click <- mouseDown V.BLeft
503− a <- child
504− return (() <$ click, a)
505−
506−-- | Retrieve the current orientation of a 'Layout'
507−askOrientation :: Monad m => Layout t m (Dynamic t Orientation)
508−askOrientation = Layout $ asks _layoutCtx_orientation
509
508
510−-}
509+-- | A widget that is focusable and occupies a layout region based on the
510+-- provided constraint. Returns the 'FocusId' allowing for manual focus
511+-- management.
512+tile'
513+ :: (MonadFix m, Reflex t, HasInput t m, HasFocus t m, HasLayout t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
514+ => Dynamic t Constraint
515+ -> m a
516+ -> m (FocusId, a)
517+tile' c w = do
518+ fid <- makeFocus
519+ r <- region c
520+ parentFocused <- isFocused fid
521+ rec (click, result, childFocused) <- pane r focused $ anyChildFocused $ \childFoc -> do
522+ m <- mouseDown V.BLeft
523+ x <- w
524+ pure (m, x, childFoc)
525+ let focused = (||) <$> parentFocused <*> childFocused
526+ requestFocus $ Refocus_Id fid <$ click
527+ pure (fid, result)
528+
529+-- | A widget that is focusable and occupies a layout region based on the
530+-- provided constraint.
531+tile
532+ :: (MonadFix m, Reflex t, HasInput t m, HasFocus t m, HasLayout t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
533+ => Dynamic t Constraint
534+ -> m a
535+ -> m a
536+tile c = fmap snd . tile' c
511
537
512−-- | Produces an 'Event' that navigates forward one tile when the Tab key is pressed
538+-- *** Unfocusable
513−-- and backward one tile when Shift+Tab is pressed.
514−tabNavigation :: (Reflex t, Monad m) => VtyWidget t m (Event t Int)
515−tabNavigation = do
516− fwd <- fmap (const 1) <$> key (V.KChar '\t')
517− back <- fmap (const (-1)) <$> key V.KBackTab
518− return $ leftmost [fwd, back]
519
539
520−test :: IO ()
540+-- | A widget that is not focusable and occupies a layout region based on the
521−test = mainWidget $ do
541+-- provided constraint.
522− inp <- input
542+grout
523− dw <- displayWidth
543+ :: (Reflex t, HasLayout t m, HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
524− dh <- displayHeight
544+ => Dynamic t Constraint
525− let r = Region <$> 0 <*> 0 <*> dw <*> dh
545+ -> m a
526− text' t = do
546+ -> m a
527− f <- focus
547+grout c w = do
528− richText (RichTextConfig $ current $ (\x -> if x then V.withStyle V.defAttr V.bold else V.defAttr) <$> f) t
548+ r <- region c
529− tab <- tabNavigation
549+ pane r (pure True) w
530− rec (_, focusSeq) <- runFocus (updated sel) $ runLayout (pure Orientation_Column) r $ do -- TODO start focused
531− col (Constraint_Min 0) $ do
532− tile $ text' "asdf"
533− tile $ text' "asdf"
534− row (Constraint_Min 0) $ do
535− tile $ text' "xyz"
536− tile $ text' "xyz"
537− tile $ text' "xyz"
538− tile $ text' "xyz"
539− tile $ text' "asdf"
540− tile $ text' "asdf"
541− ix <- foldDyn (+) 0 tab
542− let sel = Seq.lookup <$> (mod <$> ix <*> (Seq.length <$> focusSeq)) <*> focusSeq -- TODO this sucks: when elements are added your focus will shift (OSet)
543− return $ fforMaybe inp $ \case
544− V.EvKey (V.KChar 'c') [V.MCtrl] -> Just ()
545− _ -> Nothing
546−
547− -- TODO These functions shouldn't be so higher order
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