on develop help sign in
20 files changed+2275−1369
since splitsnapshots
unifiedsplit
.github/workflows/haskell.yml+12−10 .travis.yml−112 ChangeLog.md+91−3 reflex-vty.cabal+63−11 src-bin/Example/CPU.hs+231 src-bin/example.hs+213−151 src/Control/Monad/NodeId.hs+3−1 src/Data/Text/Zipper.hs+280−157 src/Reflex/Class/Switchable.hs−27 src/Reflex/Vty.hs+6 src/Reflex/Vty/Widget.hs+465−610 src/Reflex/Vty/Widget/Box.hs+124 src/Reflex/Vty/Widget/Input.hs+31−11 src/Reflex/Vty/Widget/Input/Mouse.hs+106 src/Reflex/Vty/Widget/Input/Text.hs+18−13 src/Reflex/Vty/Widget/Layout.hs+315−263 src/Reflex/Vty/Widget/Split.hs+92 src/Reflex/Vty/Widget/Text.hs+106 test/Data/Text/ZipperSpec.hs+104 test/Spec.hs+15
.github/workflows/haskell.yml+12−10
@@ -1,33 +1,33 @@
1−name: Haskell CI
1+name: github-action
22
33on: [push, pull_request]
44
55jobs:
66 build:
7−
8− runs-on: ${{ matrix.os }}
97 strategy:
108 matrix:
11− ghc: ['8.4.4', '8.6.5', '8.8.4']
9+ ghc: ['8.4.4', '8.6.5', '8.8.4', '8.10.2']
1210 os: ['ubuntu-latest', 'macos-latest']
11+ runs-on: ${{ matrix.os }}
12+
13+ name: GHC ${{ matrix.ghc }} on ${{ matrix.os }}
1314 steps:
1415 - uses: actions/checkout@v2
1516 - uses: actions/setup-haskell@v1
1617 with:
1718 ghc-version: ${{ matrix.ghc }}
18− cabal-version: '3.0'
19−
2019 - name: Cache
2120 uses: actions/cache@v1
2221 env:
2322 cache-name: cache-cabal
2423 with:
2524 path: ~/.cabal
26− key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/*.cabal') }}-${{ hashFiles('**/cabal.project') }}
25+ key: ${{ runner.os }}-${{ matrix.ghc }}-build-${{ env.cache-name }}-${{ hashFiles('**/*.cabal') }}-${{ hashFiles('**/cabal.project') }}
2726 restore-keys: |
28− ${{ runner.os }}-build-${{ env.cache-name }}-
29− ${{ runner.os }}-build-
30− ${{ runner.os }}-
27+ ${{ runner.os }}-${{ matrix.ghc }}-build-${{ env.cache-name }}-
28+ ${{ runner.os }}-${{ matrix.ghc }}-build-
29+ ${{ runner.os }}-${{ matrix.ghc }}-
30+ ${{ runner.os }}
3131
3232 - name: Install dependencies
3333 run: |
@@ -37,3 +37,5 @@ jobs:
3737 run: cabal build --enable-tests --enable-benchmarks all
3838 - name: Run tests
3939 run: cabal test all
40+ - name: Build Docs
41+ run: cabal haddock
.travis.ymldeleted−112
@@ -1,112 +0,0 @@
1−# This Travis job script has been generated by a script via
2−#
3−# haskell-ci 'reflex-vty.cabal' '--output' '.travis.yml'
4−#
5−# For more information, see https://github.com/haskell-CI/haskell-ci
6−#
7−# version: 0.3
8−#
9−language: c
10−dist: xenial
11−
12−git:
13− submodules: false # whether to recursively clone submodules
14−
15−cache:
16− directories:
17− - $HOME/.cabal/packages
18− - $HOME/.cabal/store
19−
20−before_cache:
21− - rm -fv $CABALHOME/packages/hackage.haskell.org/build-reports.log
22− # remove files that are regenerated by 'cabal update'
23− - rm -fv $CABALHOME/packages/hackage.haskell.org/00-index.*
24− - rm -fv $CABALHOME/packages/hackage.haskell.org/*.json
25− - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.cache
26− - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar
27− - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar.idx
28−
29− - rm -rfv $CABALHOME/packages/head.hackage
30−
31−matrix:
32− include:
33− - compiler: "ghc-8.8.3"
34− addons: {apt: {packages: [ghc-ppa-tools,cabal-install-3.0,ghc-8.8.3], sources: [hvr-ghc]}}
35− - compiler: "ghc-8.6.5"
36− addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-8.6.5], sources: [hvr-ghc]}}
37− - compiler: "ghc-8.4.4"
38− addons: {apt: {packages: [ghc-ppa-tools,cabal-install-2.4,ghc-8.4.4], sources: [hvr-ghc]}}
39−
40−
41−before_install:
42− - HC=/opt/ghc/bin/${CC}
43− - HCPKG=${HC/ghc/ghc-pkg}
44− - unset CC
45− - CABAL=/opt/ghc/bin/cabal
46− - CABALHOME=$HOME/.cabal
47− - export PATH="$CABALHOME/bin:$PATH"
48− - ROOTDIR=$(pwd)
49− - HCNUMVER=$(( $(${HC} --numeric-version|sed -E 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\1 * 10000 + \2 * 100 + \3/') ))
50− - echo $HCNUMVER
51−
52−install:
53− - ${CABAL} --version
54− - echo "$(${HC} --version) [$(${HC} --print-project-git-commit-id 2> /dev/null || echo '?')]"
55− - TEST=--enable-tests
56− - BENCH=--enable-benchmarks
57− - GHCHEAD=${GHCHEAD-false}
58− - travis_retry ${CABAL} update -v
59− - sed -i.bak 's/^jobs:/-- jobs:/' $CABALHOME/config
60− - rm -fv cabal.project cabal.project.local
61− - grep -Ev -- '^\s*--' $CABALHOME/config | grep -Ev '^\s*$'
62− - rm -f cabal.project
63− - touch cabal.project
64− - "printf 'packages: \".\"\\n' >> cabal.project"
65− - "printf 'write-ghc-environment-files: always\\n' >> cabal.project"
66− - touch cabal.project.local
67− - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | grep -vE -- '^(reflex-vty)$' | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done"
68− - cat cabal.project || true
69− - cat cabal.project.local || true
70− - if [ -f "./configure.ac" ]; then (cd "." && autoreconf -i); fi
71− - rm -f cabal.project.freeze
72− - ${CABAL} new-freeze -w ${HC} ${TEST} ${BENCH} --project-file="cabal.project" --dry
73− - "cat \"cabal.project.freeze\" | sed -E 's/^(constraints: *| *)//' | sed 's/any.//'"
74− - rm "cabal.project.freeze"
75− - ${CABAL} new-build -w ${HC} ${TEST} ${BENCH} --project-file="cabal.project" --dep -j2 all
76− - ${CABAL} new-build -w ${HC} --disable-tests --disable-benchmarks --project-file="cabal.project" --dep -j2 all
77− - rm -rf .ghc.environment.* "."/dist
78− - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)
79−
80−# Here starts the actual work to be performed for the package under test;
81−# any command which exits with a non-zero exit code causes the build to fail.
82−script:
83− # test that source-distributions can be generated
84− - ${CABAL} new-sdist all
85− - mv dist-newstyle/sdist/*.tar.gz ${DISTDIR}/
86− - cd ${DISTDIR} || false
87− - find . -maxdepth 1 -name '*.tar.gz' -exec tar -xvf '{}' \;
88− - rm -f cabal.project
89− - touch cabal.project
90− - "printf 'packages: \"reflex-vty-*/*.cabal\"\\n' >> cabal.project"
91− - "printf 'write-ghc-environment-files: always\\n' >> cabal.project"
92− - touch cabal.project.local
93− - "for pkg in $($HCPKG list --simple-output); do echo $pkg | sed 's/-[^-]*$//' | grep -vE -- '^(reflex-vty)$' | sed 's/^/constraints: /' | sed 's/$/ installed/' >> cabal.project.local; done"
94− - cat cabal.project || true
95− - cat cabal.project.local || true
96− # this builds all libraries and executables (without tests/benchmarks)
97− - ${CABAL} new-build -w ${HC} --disable-tests --disable-benchmarks all
98−
99− # build & run tests, build benchmarks
100− - ${CABAL} new-build -w ${HC} ${TEST} ${BENCH} all
101−
102− # cabal check
103− - (cd reflex-vty-* && ${CABAL} check)
104−
105− # haddock
106− - ${CABAL} new-haddock -w ${HC} ${TEST} ${BENCH} all
107−
108− # Build without installed constraints for packages in global-db
109− - rm -f cabal.project.local; ${CABAL} new-build -w ${HC} --disable-tests --disable-benchmarks all
110−
111−# REGENDATA ["reflex-vty.cabal","--output",".travis.yml"]
112−# EOF
ChangeLog.md+91−3
@@ -1,8 +1,96 @@
11# Revision history for reflex-vty
22
3−## Unreleased
4−* Remove text-icu dependency and switch to `wcwidth` from vty package to compute character width in `Data.Text.Zipper`.
5−* `goToDisplayLinePosition` in `Data.Text.Zipper` correctly accounts for character width now.
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_:
11+ * Remove text-icu dependency and switch to `wcwidth` from vty package to compute character width in `Data.Text.Zipper`.
12+ * `goToDisplayLinePosition` in `Data.Text.Zipper` correctly accounts for character width now.
13+ * [#37](https://github.com/reflex-frp/reflex-vty/issues/37) `Layout` should support focus changes through nested layouts (thanks @pdlla for getting this started -- see entry on Layout and Focus below).
14+ * Fix distribution of available space when it cannot be evenly distributed. Previously, all leftover space would be allocated to the first stretchable widget.
15+* _Breaking Changes_:
16+ * Layout and focus have been substantially refactored to fix [#37](https://github.com/reflex-frp/reflex-vty/issues/37) and support a wider variety of layouts and focus switching requirements.
17+ * Added a new `HasFocus` class (the old one is now `HasFocusReader`) to produce focusable elements, and manage focus state. See the "Focus" section of the Reflex.Vty.Widget.Layout module documentation.
18+ * `Layout` no longer has any focus-tracking responsibility. See the "Layout" section of the Reflex.Vty.Widget.Layout module documentation.
19+ * `tile` no longer takes a configuration record and no longer requires that its child widget return a focus request event. Focus requests are instead handled using calls to `requestFocus` in the child widget.
20+ * Calls to `fixed` and `stretch` must now be replaced with `tile . fixed` and `tile . stretch`
21+ * `stretch` now takes a minimum size argument
22+ * Added `flex` which is equivalent to `stretch 0`
23+ * `tabNavigation` no longer returns an `Event`. Instead it calls `requestFocus` directly with the appropriate `Refocus_Shift` value.
24+ * Added `axis` (in `HasLayout`), a lower-level primitive which is used to implement `row` and `col`.
25+ * Added `region` (in `HasLayout`), which is used to claim screen real estate and used to implement `tile` and `grout`
26+ * Added `grout`, a container element that is not itself focusable (though its children can be)
27+ * Removed `VtyWidget` and replaced it with a number of separate classes and monad transformers
28+ * Replace `HasDisplaySize` with `HasDisplayRegion` which carries around a region instead of just a width and height. `displayWidth` and `displayHeight` are now functions implemented in terms of `askRegion` instead of class methods.
29+ * Add a `DisplayRegion` monad transformer
30+ * Rename `ImageWriter` to `HasImageWriter`
31+ * Introduce an `ImageWriter` monad transformer
32+ * Rename `HasFocus` to `HasFocusReader`
33+ * Introduce a `FocusReader` monad transformer
34+ * Replace `HasVtyInput` with `HasInput`
35+ * Introduce an `Input` monad transformer
36+ * Introduce `HasTheme` reader class to allow setting Vty attributes of all built-in widgets
37+ * Introduce `ThemeReader` monad transformer
38+ * Remove `DynRegion` and `currentRegion`. Use `Dynamic t Region` and `current` instead. This also changes the type of `pane`'s argument.
39+ * `CheckboxConfig` now has a field taking an `Event` to set the value of the checkbox.
40+ * `checkbox` now accepts keyboard input (spacebar to check and uncheck) and is displayed in bold when focused.
41+ * `HasInput` (formerly `HasVtyInput`) now has a method `localInput` for filtering the input a child widget may receive
42+ * `HasImageWriter` now has a method `mapImages` for transforming the images emitted by a child widget
43+ * `boxTitle` now takes a `Behavior t Text` as its title, instead of a plain `Text`
44+ * `fill` now takes a `Behavior t Char` instead of a `Char`
45+ * The following functions are no longer specialized to `VtyWidget`:
46+ * `pane`: Now requires `HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m`
47+ * `drag`: Now requires `HasInput`
48+ * `mouseDown`: Now requires `HasInput`
49+ * `mouseUp`: Now requires `HasInput`
50+ * `mouseScroll`: Now requires `HasInput`
51+ * `key`: Now requires `HasInput`
52+ * `keys`: Now requires `HasInput`
53+ * `keyCombo`: Now requires `HasInput`
54+ * `keyCombos`: Now requires `HasInput`
55+ * `splitV`: Now requires `HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m`
56+ * `splitH`: Now requires `HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m`
57+ * `splitVDrag`: Now requires `HasDisplayRegion t m, HasInput t m, HasImageWriter t m, HasFocusReader t m`
58+ * `fill`: Now requires `HasImageWriter` and `HasDisplayRegion`
59+ * `boxTitle`: Now requires `HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m, HasTheme t m`
60+ * `box`: Now requires `HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m, HasTheme t m`
61+ * `boxStatic`: Now requires `HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m, HasTheme t m`
62+ * `richText`: Now requires `HasImageWriter`, and `HasDisplayRegion`
63+ * `scrollableText`: Now requires `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
64+ * `blank`: Now requires `Monad`
65+ * `button`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
66+ * `textButton`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
67+ * `textButtonStatic`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
68+ * `link`: Now requires `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
69+ * `checkbox`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, and `HasDisplayRegion`
70+ * TextZipper interface changes
71+ * `_displayLines_offsetMap` type changed to `OffsetMapWithAlignment`
72+ * `_displayLines_cursorY` replaced with `_displayLines_cursorPos` which include X position
73+ * some exposed methods intended for internal use only have been removed
74+ * `textInput`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
75+ * `multilineTextInput`: Now requires `HasFocusReader`, `HasInput`, `HasImageWriter`, `HasTheme`, and `HasDisplayRegion`
76+ * `textInputTile`: Now requires `HasFocusReader`, `HasInput`, `HasLayout`, `HasTheme`, and `HasFocus`
77+* _Misc_:
78+ * (#40 Add alignment support to TextZipper)[https://github.com/reflex-frp/reflex-vty/pull/40]
79+ * Add alignment (left/center/right) support to TextZipper
80+ * Add basic unit tests for newly created alignment methods in TextZipper
81+ * Add default instances for `HasInput`, `HasFocus`, and `HasImageWriter`
82+ * Export `withinImage` and add `imagesInRegion` to crop images to a region
83+ * Add `anyChildFocused`, which provides information about whether subwidgets are focused
84+ * Add `filterKeys`, which is the same as `localInput` but only cares about keyboard events
85+ * Add `hoistRunLayout` to apply a transformation to the context of a `Layout` action and run that action
86+ * Add various `MFunctor` instances
87+ * Add a CPU usage indicator to the example executable
88+
89+## 0.1.4.2
90+
91+* Wider bounds for GHC 8.10 support
92+
93+## 0.1.4.1
694
795## 0.1.4.1
896* Migrate to new dependent-sum / dependent-map (after the "some" package split)
reflex-vty.cabal+63−11
@@ -1,5 +1,5 @@
11name: reflex-vty
2−version: 0.1.4.1
2+version: 0.2.0.0
33synopsis: Reflex FRP host and widgets for VTY applications
44description:
55 Build terminal applications using functional reactive programming (FRP) with Reflex FRP (<https://reflex-frp.org>).
@@ -12,27 +12,35 @@ maintainer: maintainer@obsidian.systems
1212copyright: 2020 Obsidian Systems LLC
1313category: FRP
1414build-type: Simple
15−cabal-version: >=1.18
15+cabal-version: 1.18
1616extra-source-files:
1717 README.md
1818 ChangeLog.md
1919extra-doc-files: doc/tasks.png
20−tested-with: GHC ==8.8.4 || ==8.6.5 || ==8.4.4
20+tested-with:
21+ GHC ==8.4.4 || ==8.6.5 || ==8.8.4 || ==8.10.2
22+
23+source-repository head
24+ type: git
25+ location: https://github.com/reflex-frp/reflex-vty
2126
2227library
2328 exposed-modules: Reflex.Vty
2429 , Reflex.Vty.Host
2530 , Reflex.Vty.Widget
31+ , Reflex.Vty.Widget.Box
2632 , Reflex.Vty.Widget.Input
33+ , Reflex.Vty.Widget.Input.Mouse
2734 , Reflex.Vty.Widget.Input.Text
2835 , Reflex.Vty.Widget.Layout
36+ , Reflex.Vty.Widget.Split
37+ , Reflex.Vty.Widget.Text
2938 , Data.Text.Zipper
30− , Reflex.Class.Switchable
3139 , Reflex.Spider.Orphans
3240 , Control.Monad.NodeId
3341 build-depends:
34− base >= 4.10.0 && < 4.14,
35− bimap >= 0.3.3 && < 0.4,
42+ base >= 4.10.0 && < 4.15,
43+ bimap >= 0.3.3 && < 0.5,
3644 containers >= 0.5.0 && < 0.7,
3745 mtl >= 2.2.2 && < 2.3,
3846 transformers >= 0.5.5 && < 0.6,
@@ -42,6 +50,7 @@ library
4250 text >= 1.2.3 && < 1.3,
4351 dependent-sum >= 0.7 && < 0.8,
4452 exception-transformers >= 0.4.0 && < 0.5,
53+ mmorph >= 1.1 && < 1.2,
4554 ordered-containers >= 0.2.2 && < 0.3,
4655 primitive >= 0.6.3 && < 0.8,
4756 ref-tf >= 0.4.0 && < 0.5,
@@ -81,11 +90,10 @@ library
8190 TypeApplications
8291 TypeFamilies
8392
84−
8593executable example
8694 hs-source-dirs: src-bin
8795 main-is: example.hs
88− ghc-options: -threaded
96+ ghc-options: -threaded -Wall
8997 build-depends:
9098 base,
9199 containers,
@@ -96,7 +104,51 @@ executable example
96104 transformers,
97105 vty
98106 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
99137
100−source-repository head
101− type: git
102− location: https://github.com/reflex-frp/reflex-vty
138+
139+test-suite reflex-vty-test
140+ hs-source-dirs: test
141+ main-is: Spec.hs
142+ type: exitcode-stdio-1.0
143+ ghc-options: -threaded
144+ other-modules:
145+ Data.Text.ZipperSpec
146+ build-depends:
147+ base,
148+ reflex,
149+ containers,
150+ reflex-vty,
151+ text,
152+ extra,
153+ hspec >= 2.7 && < 2.8
154+ default-language: Haskell2010
src-bin/Example/CPU.hsnew file+231
@@ -0,0 +1,231 @@
1+{-|
2+ Description: A CPU usage indicator
3+-}
4+module Example.CPU where
5+
6+import Control.Exception
7+import Control.Monad.Fix
8+import Control.Monad.IO.Class
9+import Data.Ratio
10+import Data.Time
11+import Data.Word
12+import qualified Data.Text as T
13+import qualified Graphics.Vty as V
14+import Text.Printf
15+
16+import Reflex
17+import Reflex.Vty
18+
19+-- | Each constructor represents a cpu statistic column as presented in @/proc/stat@
20+data CpuStat
21+ = CpuStat_User
22+ | CpuStat_Nice
23+ | CpuStat_System
24+ | CpuStat_Idle
25+ | CpuStat_Iowait
26+ | CpuStat_Irq
27+ | CpuStat_Softirq
28+ | CpuStat_Steal
29+ | CpuStat_Guest
30+ | CpuStat_GuestNice
31+ deriving (Show, Read, Eq, Ord, Enum, Bounded)
32+
33+-- | Read @/proc/stat@
34+getCpuStat :: IO (Maybe (CpuStat -> Word64))
35+getCpuStat = do
36+ s <- readFile "/proc/stat"
37+ _ <- evaluate $ length s -- Make readFile strict
38+ pure $ do
39+ cpuSummaryLine : _ <- pure $ lines s
40+ [user, nice, system, idle, iowait, irq, softirq, steal, guest, guestNice] <- pure $ map read $ words $ drop 4 cpuSummaryLine
41+ pure $ \case
42+ CpuStat_User -> user
43+ CpuStat_Nice -> nice
44+ CpuStat_System -> system
45+ CpuStat_Idle -> idle
46+ CpuStat_Iowait -> iowait
47+ CpuStat_Irq -> irq
48+ CpuStat_Softirq -> softirq
49+ CpuStat_Steal -> steal
50+ CpuStat_Guest -> guest
51+ CpuStat_GuestNice -> guestNice
52+
53+sumStats :: (CpuStat -> Word64) -> [CpuStat] -> Word64
54+sumStats get stats = sum $ get <$> stats
55+
56+-- | user + nice + system + irq + softirq + steal
57+nonIdleStats :: [CpuStat]
58+nonIdleStats =
59+ [ CpuStat_User
60+ , CpuStat_Nice
61+ , CpuStat_System
62+ , CpuStat_Irq
63+ , CpuStat_Softirq
64+ , CpuStat_Steal
65+ ]
66+
67+-- | idle + iowait
68+idleStats :: [CpuStat]
69+idleStats =
70+ [ CpuStat_Idle
71+ , CpuStat_Iowait
72+ ]
73+
74+-- | Draws the cpu usage percent as a live-updating bar graph. The output should look like:
75+--
76+-- > ╔══════ CPU Usage: 38% ══════╗
77+-- > ║ ║
78+-- > ║ ║
79+-- > ║ ║
80+-- > ║ ║
81+-- > ║ ║
82+-- > ║ ║
83+-- > ║█████████████████████████████║
84+-- > ║█████████████████████████████║
85+-- > ║█████████████████████████████║
86+-- > ║█████████████████████████████║
87+-- > ╚═════════════════════════════╝
88+--
89+cpuStats
90+ :: ( Reflex t
91+ , MonadFix m
92+ , MonadHold t m
93+ , MonadIO (Performable m)
94+ , MonadIO m
95+ , PerformEvent t m
96+ , PostBuild t m
97+ , TriggerEvent t m
98+ , HasDisplayRegion t m
99+ , HasImageWriter t m
100+ , HasLayout t m
101+ , HasFocus t m
102+ , HasInput t m
103+ , HasFocusReader t m, HasTheme t m
104+ )
105+ => m ()
106+cpuStats = do
107+ tick <- tickLossy 0.25 =<< liftIO getCurrentTime
108+ cpuStat :: Event t (Word64, Word64) <- fmap (fmapMaybe id) $
109+ performEvent $ ffor tick $ \_ -> do
110+ get <- liftIO getCpuStat
111+ pure $ case get of
112+ Nothing -> Nothing
113+ Just get' -> Just (sumStats get' nonIdleStats, sumStats get' idleStats)
114+ active <- foldDyn cpuPercentStep ((0, 0), 0) cpuStat
115+ let pct = fmap snd active
116+ chart pct
117+
118+chart
119+ :: ( MonadFix m
120+ , HasFocus t m
121+ , HasLayout t m
122+ , HasImageWriter t m
123+ , HasInput t m
124+ , HasDisplayRegion t m
125+ , HasFocusReader t m, HasTheme t m
126+ )
127+ => Dynamic t (Ratio Word64) -> m ()
128+chart pct = do
129+ let title = ffor pct $ \x -> mconcat
130+ [ " CPU Usage: "
131+ , T.pack (printf "%3d" $ (ceiling $ x * 100 :: Int))
132+ , "% "
133+ ]
134+ boxTitle (pure doubleBoxStyle) (current title) $ col $ do
135+ grout flex blank
136+ dh <- displayHeight
137+ let heights = calcRowHeights <$> dh <*> pct
138+ quarters = fst <$> heights
139+ eighths = snd <$> heights
140+ eighthRow = ffor eighths $ \x -> if x == 0 then 0 else 1
141+ grout (fixed eighthRow) $ fill' (current $ eighthBlocks <$> eighths) $ current $
142+ ffor quarters $ \q ->
143+ if | _quarter_fourth q > 0 -> red
144+ | _quarter_third q > 0 -> orange
145+ | _quarter_second q > 0 -> yellow
146+ | otherwise -> white
147+ grout (fixed $ _quarter_fourth <$> quarters) $ fill' (pure '█') (pure red)
148+ grout (fixed $ _quarter_third <$> quarters) $ fill' (pure '█') (pure orange)
149+ grout (fixed $ _quarter_second <$> quarters) $ fill' (pure '█') (pure yellow)
150+ grout (fixed $ _quarter_first <$> quarters) $ fill' (pure '█') (pure white)
151+ where
152+ -- Calculate number of full rows, height of partial row
153+ calcRowHeights :: Int -> Ratio Word64 -> (Quarter Int, Int)
154+ calcRowHeights h r =
155+ let (full, leftovers) = divMod (numerator r * fromIntegral h) (denominator r)
156+ partial = ceiling $ 8 * (leftovers % denominator r)
157+ quarter = ceiling $ fromIntegral h / (4 :: Double)
158+ n = fromIntegral full
159+ in if | n <= quarter ->
160+ (Quarter n 0 0 0, partial)
161+ | n <= (2 * quarter) ->
162+ (Quarter quarter (n - quarter) 0 0, partial)
163+ | n <= (3 * quarter) ->
164+ (Quarter quarter quarter (n - (2 * quarter)) 0, partial)
165+ | otherwise ->
166+ (Quarter quarter quarter quarter (n - (3 * quarter)), partial)
167+ fill' bc attr = do
168+ dw <- displayWidth
169+ dh <- displayHeight
170+ let fillImg =
171+ (\w h c a -> [V.charFill a c w h])
172+ <$> current dw
173+ <*> current dh
174+ <*> bc
175+ <*> attr
176+ tellImages fillImg
177+ color :: Int -> Int -> Int -> V.Color
178+ color = V.rgbColor
179+ red = V.withForeColor V.defAttr $ color 255 0 0
180+ orange = V.withForeColor V.defAttr $ color 255 165 0
181+ yellow = V.withForeColor V.defAttr $ color 255 255 0
182+ white = V.withForeColor V.defAttr $ color 255 255 255
183+
184+data Quarter a = Quarter
185+ { _quarter_first :: a
186+ , _quarter_second :: a
187+ , _quarter_third :: a
188+ , _quarter_fourth :: a
189+ }
190+
191+eighthBlocks :: (Eq a, Num a, Ord a) => a -> Char
192+eighthBlocks n =
193+ if
194+ | n <= 0 -> ' '
195+ | n == 1 -> '▁'
196+ | n == 2 -> '▂'
197+ | n == 3 -> '▃'
198+ | n == 4 -> '▄'
199+ | n == 5 -> '▅'
200+ | n == 6 -> '▆'
201+ | n == 7 -> '▇'
202+ | otherwise -> '█'
203+
204+-- | Determine the current percentage usage according to this algorithm:
205+--
206+-- PrevIdle = previdle + previowait
207+-- Idle = idle + iowait
208+--
209+-- PrevNonIdle = prevuser + prevnice + prevsystem + previrq + prevsoftirq + prevsteal
210+-- NonIdle = user + nice + system + irq + softirq + steal
211+--
212+-- PrevTotal = PrevIdle + PrevNonIdle
213+-- Total = Idle + NonIdle
214+--
215+-- totald = Total - PrevTotal
216+-- idled = Idle - PrevIdle
217+--
218+-- CPU_Percentage = (totald - idled)/totald
219+--
220+-- Source: https://stackoverflow.com/questions/23367857/accurate-calculation-of-cpu-usage-given-in-percentage-in-linux
221+cpuPercentStep
222+ :: (Word64, Word64) -- Current active, Current idle
223+ -> ((Word64, Word64), Ratio Word64) -- (Previous idle, Previous total), previous percent
224+ -> ((Word64, Word64), Ratio Word64) -- (New idle, new total), percent
225+cpuPercentStep (nonidle, idle) ((previdle, prevtotal), _) =
226+ let total = idle + nonidle
227+ idled = idle - previdle
228+ totald = total - prevtotal
229+ in ( (idle, total)
230+ , (totald - idled) % totald
231+ )
src-bin/example.hs+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−
121import Control.Applicative
132import Control.Monad
143import Control.Monad.Fix
15−import Control.Monad.NodeId
4+import Data.Functor
165import Data.Functor.Misc
176import Data.Map (Map)
18−import Data.Maybe
197import qualified Data.Map as Map
8+import qualified Data.Set as Set
209import Data.Text (Text)
2110import qualified Data.Text as T
2211import qualified Data.Text.Zipper as TZ
2312import qualified Graphics.Vty as V
2413import Reflex
2514import Reflex.Network
26−import Reflex.Class.Switchable
2715import Reflex.Vty
2816
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+
2934data Example = Example_TextEditor
3035 | Example_Todo
3136 | Example_ScrollableTextDisplay
37+ | Example_ClickButtonsGetEmojis
38+ | Example_CPUStat
3239 deriving (Show, Read, Eq, Ord, Enum, Bounded)
3340
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+
3457main :: 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."
36119 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
61120 return $ fforMaybe inp $ \case
62121 V.EvKey (V.KChar 'c') [V.MCtrl] -> Just ()
63122 _ -> 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
65134taskList
66− :: (Reflex t, MonadHold t m, MonadFix m, Adjustable t m, NotReady t m, PostBuild t m, MonadNodeId m)
67− => VtyWidget t m ()
68−taskList = do
69− let btn = textButtonStatic def "Add another task"
70− inp <- input
135+ :: (VtyExample t m, Manager t m, MonadHold t m, Adjustable t m, PostBuild t m)
136+ => m ()
137+taskList = col $ do
71138 let todos0 =
72139 [ Todo "Find reflex-vty" True
73140 , Todo "Become functional reactive" False
74141 , Todo "Make vty apps" False
75142 ]
76− rec let todos' = todos todos0 $ leftmost
77− [ () <$ e
78− , fforMaybe inp $ \case
79− V.EvKey V.KEnter [] -> Just ()
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)
143+ btn = textButtonStatic def "Add another task"
144+ enter <- fmap (const ()) <$> key V.KEnter
145+ rec void $ grout flex $ todos todos0 $ enter <> click
146+ click <- tile (fixed 3) btn
84147 return ()
85148
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−
128149data Todo = Todo
129150 { _todo_label :: Text
130151 , _todo_done :: Bool
@@ -135,87 +156,128 @@ data TodoOutput t = TodoOutput
135156 { _todoOutput_todo :: Dynamic t Todo
136157 , _todoOutput_delete :: Event t ()
137158 , _todoOutput_height :: Dynamic t Int
159+ , _todoOutput_focusId :: FocusId
138160 }
139161
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−
146162todo
147− :: (MonadHold t m, MonadFix m, Reflex t, MonadNodeId m)
163+ :: (VtyExample t m, Manager t m, MonadHold t m)
148164 => Todo
149− -> VtyWidget t m (TodoOutput t)
150−todo t0 = do
151− w <- displayWidth
152− rec let checkboxWidth = 3
153− checkboxRegion = DynRegion 0 0 checkboxWidth 1
154− labelHeight = _textInput_lines ti
155− labelWidth = w - 1 - checkboxWidth
156− labelLeft = checkboxWidth + 1
157− labelTop = constDyn 0
158− labelRegion = DynRegion labelLeft labelTop labelWidth labelHeight
159− value <- pane checkboxRegion (pure True) $ checkbox def $ _todo_done t0
160− (ti, d) <- pane labelRegion (pure True) $ do
161− i <- input
162− v <- textInput $ def { _textInputConfig_initialValue = TZ.fromText $ _todo_label t0 }
163− let deleteSelf = attachWithMaybe backspaceOnEmpty (current $ _textInput_value v) i
164− return (v, deleteSelf)
165− return $ TodoOutput
166− { _todoOutput_todo = Todo <$> _textInput_value ti <*> value
167− , _todoOutput_delete = d
168− , _todoOutput_height = _textInput_lines ti
169− }
165+ -> m (TodoOutput t)
166+todo t0 = row $ do
167+ let toggleKeys = Set.fromList
168+ [ (V.KChar ' ', [V.MCtrl])
169+ , (V.KChar '@', [V.MCtrl])
170+ ]
171+ anyChildFocused $ \focused -> do
172+ toggleE <- keyCombos toggleKeys
173+ filterKeys (flip Set.notMember $ Set.insert (V.KChar '\t', []) toggleKeys) $ do
174+ rec let cfg = def
175+ { _checkboxConfig_setValue = setVal
176+ }
177+ value <- tile (fixed 4) $ checkbox cfg $ _todo_done t0
178+ let setVal = attachWith (\v _ -> not v) (current value) $ gate (current focused) toggleE
179+ (fid, (ti, d)) <- tile' flex $ do
180+ i <- input
181+ v <- textInput $ def
182+ { _textInputConfig_initialValue = TZ.fromText $ _todo_label t0 }
183+ let deleteSelf = attachWithMaybe backspaceOnEmpty (current $ _textInput_value v) i
184+ return (v, deleteSelf)
185+ return $ TodoOutput
186+ { _todoOutput_todo = Todo <$> _textInput_value ti <*> value
187+ , _todoOutput_delete = d
188+ , _todoOutput_height = _textInput_lines ti
189+ , _todoOutput_focusId = fid
190+ }
170191 where
171192 backspaceOnEmpty v = \case
172193 V.EvKey V.KBS _ | T.null v -> Just ()
173194 _ -> Nothing
174195
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−
181196todos
182197 :: forall t m.
183198 ( MonadHold t m
184− , MonadFix m
185− , Reflex t
199+ , Manager t m
200+ , VtyExample t m
186201 , Adjustable t m
187− , NotReady t m
188202 , PostBuild t m
189− , MonadNodeId m
190203 )
191204 => [Todo]
192205 -> Event t ()
193− -> VtyWidget t m (Dynamic t (Map Int (TodoOutput t)))
206+ -> m (Dynamic t (Map Int (TodoOutput t)))
194207todos todos0 newTodo = do
195208 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
202− let sel = select selectOnDelete $ Const2 k
203− click <- void <$> mouseDown V.BLeft
204− pb <- getPostBuild
205− let focusMe = leftmost [ click, sel, pb ]
206− r <- todo t
207− return (focusMe, r)
208− let delete = ffor todoDelete $ \k -> Map.singleton k Nothing
209+ rec listOut <- listHoldWithKey todosMap0 updates $ \k t -> grout (fixed 1) $ do
210+ to <- todo t
211+ let sel = select selectOnDelete $ Const2 k
212+ pb <- getPostBuild
213+ requestFocus $ Refocus_Id (_todoOutput_focusId to) <$ leftmost [pb, sel]
214+ pure to
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
209220 updates = leftmost [insert, delete]
210221 todoDelete = switch . current $
211222 leftmost . Map.elems . Map.mapWithKey (\k -> (k <$) . _todoOutput_delete) <$> listOut
212− todosMap = joinDynThroughMap $ fmap _todoOutput_todo <$> listOut
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
223+
216224 selectOnDelete = fanMap $ (`Map.singleton` ()) <$> attachWithMaybe
217225 (\m k -> let (before, after) = Map.split k m
218226 in fmap fst $ Map.lookupMax before <|> Map.lookupMin after)
219227 (current todosMap)
220228 todoDelete
221229 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+3−1
@@ -10,6 +10,7 @@ module Control.Monad.NodeId
1010 , runNodeIdT
1111 ) where
1212
13+import Control.Monad.Morph
1314import Control.Monad.Reader
1415import Control.Monad.Ref
1516import Data.IORef
@@ -34,10 +35,12 @@ newtype NodeIdT m a = NodeIdT { unNodeIdT :: ReaderT (IORef NodeId) m a }
3435 deriving
3536 ( Functor
3637 , Applicative
38+ , MFunctor
3739 , Monad
3840 , MonadFix
3941 , MonadHold t
4042 , MonadIO
43+ , MonadRef
4144 , MonadReflexCreateTrigger t
4245 , MonadSample t
4346 , MonadTrans
@@ -45,7 +48,6 @@ newtype NodeIdT m a = NodeIdT { unNodeIdT :: ReaderT (IORef NodeId) m a }
4548 , PerformEvent t
4649 , PostBuild t
4750 , TriggerEvent t
48− , MonadRef
4951 )
5052
5153instance MonadNodeId m => MonadNodeId (ReaderT x m)
src/Data/Text/Zipper.hs+280−157
@@ -9,22 +9,26 @@ Description: A zipper for text documents that allows convenient editing and navi
99-}
1010module Data.Text.Zipper where
1111
12+import Prelude
13+
14+import Control.Exception (assert)
15+import Control.Monad.State (evalState, forM, get, put)
1216import Data.Char (isSpace)
1317import Data.Map (Map)
14−import qualified Data.Map as Map
1518import Data.Maybe (fromMaybe)
1619import Data.String
17−import Control.Monad.State (evalState, forM, get, put)
18−
1920import Data.Text (Text)
20−import qualified Data.Text as T
2121import Data.Text.Internal (Text(..), text)
2222import Data.Text.Internal.Fusion (stream)
2323import Data.Text.Internal.Fusion.Types (Stream(..), Step(..))
2424import Data.Text.Unsafe
25+import qualified Data.List as L
26+import qualified Data.Map as Map
27+import qualified Data.Text as T
2528
2629import Graphics.Text.Width (wcwidth)
2730
31+
2832-- | A zipper of the logical text input contents (the "document"). The lines
2933-- before the line containing the cursor are stored in reverse order.
3034-- The cursor is logically between the "before" and "after" text.
@@ -166,7 +170,7 @@ deleteLeftWord (TextZipper lb b a la) =
166170-- | Insert up to n spaces to get to the next logical column that is a multiple of n
167171tab :: Int -> TextZipper -> TextZipper
168172tab n z@(TextZipper _ b _ _) =
169− insert (T.replicate (fromEnum $ n - (T.length b `mod` max 1 n)) " ") z
173+ insert (T.replicate (fromEnum $ n - T.length b `mod` max 1 n) " ") z
170174
171175-- | The plain text contents of the zipper
172176value :: TextZipper -> Text
@@ -189,105 +193,35 @@ fromText = flip insert empty
189193data Span tag = Span tag Text
190194 deriving (Show)
191195
196+-- | Text alignment type
197+data TextAlignment =
198+ TextAlignment_Left
199+ | TextAlignment_Right
200+ | TextAlignment_Center
201+ deriving (Eq, Show)
202+
203+-- A map from the index (row) of display line to (fst,snd)
204+-- fst: leading empty spaces from left (may be negative) to adjust for alignment
205+-- snd: the text offset from the beginning of the document
206+-- to the first character of the display line
207+type OffsetMapWithAlignment = Map Int (Int, Int)
208+
209+-- helper type representing a single visual line that may be part of a wrapped logical line
210+data WrappedLine = WrappedLine
211+ { _wrappedLines_text :: Text
212+ , _wrappedLines_hiddenWhitespace :: Bool -- ^ 'True' if this line ends with a deleted whitespace character
213+ , _wrappedLines_offset :: Int -- ^ offset from beginning of line
214+ }
215+ deriving (Eq, Show)
216+
192217-- | Information about the document as it is displayed (i.e., post-wrapping)
193218data DisplayLines tag = DisplayLines
194219 { _displayLines_spans :: [[Span tag]]
195− , _displayLines_offsetMap :: Map Int Int
196− , _displayLines_cursorY :: Int
220+ , _displayLines_offsetMap :: OffsetMapWithAlignment
221+ , _displayLines_cursorPos :: (Int, Int) -- cursor position relative to upper left hand corner
197222 }
198223 deriving (Show)
199224
200−-- | Given a width and a 'TextZipper', produce a list of display lines
201−-- (i.e., lines of wrapped text) with special attributes applied to
202−-- certain segments (e.g., the cursor). Additionally, produce the current
203−-- y-coordinate of the cursor and a mapping from display line number to text
204−-- offset
205−displayLines
206− :: Int -- ^ Width, used for wrapping
207− -> tag -- ^ Metadata for normal characters
208− -> tag -- ^ Metadata for the cursor
209− -> TextZipper -- ^ The text input contents and cursor state
210− -> DisplayLines tag
211−displayLines width tag cursorTag (TextZipper lb b a la) =
212− let linesBefore :: [[Text]] -- The wrapped lines before the cursor line
213− linesBefore = map (wrapWithOffset width 0) $ reverse lb
214− linesAfter :: [[Text]] -- The wrapped lines after the cursor line
215− linesAfter = map (wrapWithOffset width 0) la
216− offsets :: Map Int Int
217− offsets = offsetMap $ mconcat
218− [ linesBefore
219− , [wrapWithOffset width 0 $ b <> a]
220− , linesAfter
221− ]
222− spansBefore = map ((:[]) . Span tag) $ concat linesBefore
223− spansAfter = map ((:[]) . Span tag) $ concat linesAfter
224− -- Separate the spans before the cursor into
225− -- * spans that are on earlier display lines (though on the same logical line), and
226− -- * spans that are on the same display line
227− (spansCurrentBefore, spansCurLineBefore) = fromMaybe ([], []) $
228− initLast $ map ((:[]) . Span tag) (wrapWithOffset width 0 b)
229− -- Calculate the number of columns on the cursor's display line before the cursor
230− curLineOffset = spansWidth spansCurLineBefore
231− -- Check whether the spans on the current display line are long enough that
232− -- the cursor has to go to the next line
233− cursorAfterEOL = curLineOffset == width
234− cursorCharWidth = case T.uncons a of
235− Nothing -> 1
236− Just (c, _) -> charWidth c
237− -- Separate the span after the cursor into
238− -- * spans that are on the same display line, and
239− -- * spans that are on later display lines (though on the same logical line)
240− (spansCurLineAfter, spansCurrentAfter) = fromMaybe ([], []) $
241− headTail $ case T.uncons a of
242− Nothing -> [[Span cursorTag " "]]
243− Just (c, rest) ->
244− let o = if cursorAfterEOL then cursorCharWidth else curLineOffset + cursorCharWidth
245− cursor = Span cursorTag (T.singleton c)
246− in case map ((:[]) . Span tag) (wrapWithOffset width o rest) of
247− [] -> [[cursor]]
248− (l:ls) -> (cursor : l) : ls
249− in DisplayLines
250− { _displayLines_spans = concat
251− [ spansBefore
252− , spansCurrentBefore
253− , if cursorAfterEOL
254− then [ spansCurLineBefore, spansCurLineAfter ]
255− else [ spansCurLineBefore <> spansCurLineAfter ]
256− , spansCurrentAfter
257− , spansAfter
258− ]
259− , _displayLines_offsetMap = offsets
260− , _displayLines_cursorY = sum
261− [ length spansBefore
262− , length spansCurrentBefore
263− , if cursorAfterEOL then 1 else 0
264− ]
265− }
266− where
267− initLast :: [a] -> Maybe ([a], a)
268− initLast = \case
269− [] -> Nothing
270− (x:xs) -> case initLast xs of
271− Nothing -> Just ([], x)
272− Just (ys, y) -> Just (x:ys, y)
273− headTail :: [a] -> Maybe (a, [a])
274− headTail = \case
275− [] -> Nothing
276− x:xs -> Just (x, xs)
277−
278−-- | Wraps a logical line of text to fit within the given width. The first
279−-- wrapped line is offset by the number of columns provided. Subsequent wrapped
280−-- lines are not.
281−wrapWithOffset
282− :: Int -- ^ Maximum width
283− -> Int -- ^ Offset for first line
284− -> Text -- ^ Text to be wrapped
285− -> [Text]
286−wrapWithOffset maxWidth _ _ | maxWidth <= 0 = []
287−wrapWithOffset maxWidth n xs =
288− let (firstLine, rest) = splitAtWidth (maxWidth - n) xs
289− in firstLine : (fmap (takeWidth maxWidth) . takeWhile (not . T.null) . iterate (dropWidth maxWidth) $ rest)
290−
291225-- | Split a 'Text' at the given column index. For example
292226--
293227-- > splitAtWidth 3 "ᄀabc" == ("ᄀa", "bc")
@@ -297,16 +231,16 @@ splitAtWidth :: Int -> Text -> (Text, Text)
297231splitAtWidth n t@(Text arr off len)
298232 | n <= 0 = (T.empty, t)
299233 | n >= textWidth t = (t, T.empty)
300− | otherwise = let k = iterNWidth n t
234+ | otherwise = let k = toLogicalIndex n t
301235 in (text arr off k, text arr (off+k) (len-k))
302− where
303− iterNWidth :: Int -> Text -> Int
304− iterNWidth n' t'@(Text _ _ len') = loop 0 0
305− where loop !i !cnt
306− | i >= len' || cnt + w > n' = i
307− | otherwise = loop (i+d) (cnt + w)
308− where Iter c d = iter t' i
309− w = charWidth c
236+
237+toLogicalIndex :: Int -> Text -> Int
238+toLogicalIndex n' t'@(Text _ _ len') = loop 0 0
239+ where loop !i !cnt
240+ | i >= len' || cnt + w > n' = i
241+ | otherwise = loop (i+d) (cnt + w)
242+ where Iter c d = iter t' i
243+ w = charWidth c
310244
311245-- | Takes the given number of columns of characters. For example
312246--
@@ -335,56 +269,6 @@ dropWidth n = snd . splitAtWidth n
335269charWidth :: Char -> Int
336270charWidth = wcwidth
337271
338−
339−-- | For a given set of wrapped logical lines, computes a map
340−-- from display line index to text offset in the original text.
341−-- This is used to help determine how interactions with the displayed
342−-- text map back to the original text.
343−-- For example, given the document @\"AA\\nBBB\\nCCCCCCCC\\n\"@ wrapped to 5 columns,
344−-- this function will compute the offset in the original document of each character
345−-- in column 1:
346−--
347−-- > AA... (0, 0)
348−-- > BBB.. (1, 3)
349−-- > CCCCC (2, 7) -- (this line wraps to the next row)
350−-- > CCC.. (3, 12)
351−-- > ..... (4, 16)
352−offsetMap
353− :: [[Text]] -- ^ The outer list represents logical lines, and the
354− -- inner list represents the display lines into which
355− -- the logical line has been wrapped
356− -> Map Int Int -- ^ A map from the index (row) of display line to
357− -- the text offset from the beginning of the document
358− -- to the first character of the display line
359−offsetMap ts = evalState (offsetMap' ts) (0, 0)
360− where
361− offsetMap' xs = fmap Map.unions $ forM xs $ \x -> do
362− maps <- forM x $ \line -> do
363− let l = T.length line
364− (dl, o) <- get
365− put (dl + 1, o + l)
366− return $ Map.singleton dl o
367− (dl, o) <- get
368− put (dl, o + 1)
369− return $ Map.insert dl (o + 1) $ Map.unions maps
370−
371−-- | Move the cursor of the given 'TextZipper' to the logical position indicated
372−-- by the given display line coordinates, using the provided 'DisplayLines'
373−-- information. If the x coordinate is beyond the end of a line, the cursor is
374−-- moved to the end of the line.
375−goToDisplayLinePosition :: Int -> Int -> DisplayLines tag -> TextZipper -> TextZipper
376−goToDisplayLinePosition x y dl tz =
377− let offset = Map.lookup y $ _displayLines_offsetMap dl
378− in case offset of
379− Nothing -> tz
380− Just o ->
381− let
382− moveRight = case drop y $ _displayLines_spans dl of
383− [] -> x
384− (s:_) -> charIndexAt x . stream . mconcat . fmap (\(Span _ t) -> t) $ s
385− in rightN (o + min moveRight x) $ top tz
386−
387−
388272-- | Get the width of the text in a set of 'Span's, taking into account unicode character widths
389273spansWidth :: [Span tag] -> Int
390274spansWidth = sum . map (\(Span _ t) -> textWidth t)
@@ -420,3 +304,242 @@ charIndexAt pos (Stream next s0 _len) = loop_length 0 0 s0
420304 Yield c s' -> if w > pos then i else loop_length (i+1) w s' where
421305 w = z + charWidth c
422306{-# INLINE[0] charIndexAt #-}
307+
308+
309+
310+
311+-- | Same as T.words except whitespace characters are included at end (i.e. ["line1 ", ...])
312+-- 'Char's representing white space.
313+wordsWithWhitespace :: Text -> [Text]
314+wordsWithWhitespace t@(Text arr off len) = loop 0 0 False
315+ where
316+ loop !start !n !wasSpace
317+ | n >= len = [Text arr (start+off) (n-start) | not (start == n)]
318+ | isSpace c = loop start (n+d) True
319+ | wasSpace = Text arr (start+off) (n-start) : loop n n False
320+ | otherwise = loop start (n+d) False
321+ where Iter c d = iter t n
322+{-# INLINE wordsWithWhitespace #-}
323+
324+-- | Split words into logical lines, 'True' in the tuple indicates line ends with a whitespace character that got deleted
325+splitWordsAtDisplayWidth :: Int -> [Text] -> [(Text, Bool)]
326+splitWordsAtDisplayWidth maxWidth wwws = reverse $ loop wwws 0 [] where
327+ appendOut :: [(Text,Bool)] -> Text -> Bool -> [(Text,Bool)]
328+ appendOut [] t b = [(t,b)]
329+ appendOut ((t',_):ts') t b = (t'<>t,b) : ts'
330+
331+ -- remove the last whitespace in output
332+ modifyOutForNewLine :: [(Text,Bool)] -> [(Text,Bool)]
333+ modifyOutForNewLine [] = error "should never happen"
334+ modifyOutForNewLine ((t',_):ts) = case T.unsnoc t' of
335+ Nothing -> error "should never happen"
336+ Just (t,lastChar) -> assert (isSpace lastChar) $ (t,True):ts -- assume last char is whitespace
337+
338+ loop :: [Text] -> Int -> [(Text,Bool)] -> [(Text,Bool)]
339+ loop [] _ out = out
340+ loop (x:xs) cumw out = r where
341+ newWidth = textWidth x + cumw
342+ r = if newWidth > maxWidth
343+ then if isSpace $ T.index x (toLogicalIndex (maxWidth - cumw) x)
344+ -- if line runs over but character of splitting is whitespace then split on the whitespace
345+ then let (t1,t2) = splitAtWidth (maxWidth - cumw) x
346+ in loop (T.drop 1 t2:xs) 0 [] <> appendOut out t1 True
347+ else if cumw == 0
348+ -- single word exceeds max width, so just split on the word
349+ then let (t1,t2) = splitAtWidth (maxWidth - cumw) x
350+ in loop (t2:xs) 0 [] <> appendOut out t1 False
351+ -- otherwise start a new line
352+ else loop (x:xs) 0 [] <> modifyOutForNewLine out
353+ else loop xs newWidth $ appendOut out x False
354+
355+
356+
357+-- | Wraps a logical line of text to fit within the given width. The first
358+-- wrapped line is offset by the number of columns provided. Subsequent wrapped
359+-- lines are not.
360+wrapWithOffsetAndAlignment
361+ :: TextAlignment
362+ -> Int -- ^ Maximum width
363+ -> Int -- ^ Offset for first line
364+ -> Text -- ^ Text to be wrapped
365+ -> [WrappedLine] -- (words on that line, hidden space char, offset from beginning of line)
366+wrapWithOffsetAndAlignment _ maxWidth _ _ | maxWidth <= 0 = []
367+wrapWithOffsetAndAlignment alignment maxWidth n txt = assert (n <= maxWidth) r where
368+ -- we pad by offset amount with any non-space character which we will remove later so that no changes need to be made to splitWordsAtDisplayWidth
369+ r' = splitWordsAtDisplayWidth maxWidth $ wordsWithWhitespace ( T.replicate n "." <> txt)
370+ fmapfn (t,b) = case alignment of
371+ TextAlignment_Left -> WrappedLine t b 0
372+ TextAlignment_Right -> WrappedLine t b (maxWidth-l)
373+ TextAlignment_Center -> WrappedLine t b ((maxWidth-l) `div` 2)
374+ where l = textWidth t
375+ r'' = case r' of
376+ [] -> []
377+ (x,b):xs -> (T.drop n x,b):xs
378+ r = fmap fmapfn r''
379+
380+-- converts deleted eol spaces into logical lines
381+eolSpacesToLogicalLines :: [[WrappedLine]] -> [[(Text, Int)]]
382+eolSpacesToLogicalLines = fmap (fmap (\(WrappedLine a _ c) -> (a,c))) . concatMap (L.groupBy (\(WrappedLine _ b _) _ -> not b))
383+
384+offsetMapWithAlignmentInternal :: [[WrappedLine]] -> OffsetMapWithAlignment
385+offsetMapWithAlignmentInternal = offsetMapWithAlignment . eolSpacesToLogicalLines
386+
387+offsetMapWithAlignment
388+ :: [[(Text, Int)]] -- ^ The outer list represents logical lines, inner list represents wrapped lines
389+ -> OffsetMapWithAlignment
390+offsetMapWithAlignment ts = evalState (offsetMap' ts) (0, 0)
391+ where
392+ offsetMap' xs = fmap Map.unions $ forM xs $ \x -> do
393+ maps <- forM x $ \(line,align) -> do
394+ let l = T.length line
395+ (dl, o) <- get
396+ put (dl + 1, o + l)
397+ return $ Map.singleton dl (align, o)
398+ (dl, o) <- get
399+ put (dl, o + 1)
400+ -- add additional offset to last line in wrapped lines (for newline char)
401+ return $ Map.adjust (\(align,_)->(align,o+1)) dl $ Map.unions maps
402+
403+
404+-- | Given a width and a 'TextZipper', produce a list of display lines
405+-- (i.e., lines of wrapped text) with special attributes applied to
406+-- certain segments (e.g., the cursor). Additionally, produce the current
407+-- y-coordinate of the cursor and a mapping from display line number to text
408+-- offset
409+displayLinesWithAlignment
410+ :: TextAlignment
411+ -> Int -- ^ Width, used for wrapping
412+ -> tag -- ^ Metadata for normal characters
413+ -> tag -- ^ Metadata for the cursor
414+ -> TextZipper -- ^ The text input contents and cursor state
415+ -> DisplayLines tag
416+displayLinesWithAlignment alignment width tag cursorTag (TextZipper lb b a la) =
417+ let linesBefore :: [[WrappedLine]] -- The wrapped lines before the cursor line
418+ linesBefore = map (wrapWithOffsetAndAlignment alignment width 0) $ reverse lb
419+ linesAfter :: [[WrappedLine]] -- The wrapped lines after the cursor line
420+ linesAfter = map (wrapWithOffsetAndAlignment alignment width 0) la
421+
422+ -- simulate trailing cursor character when computing OffsetMap
423+ afterWithCursor = if T.null a then " " else a
424+ offsets :: OffsetMapWithAlignment
425+ offsets = offsetMapWithAlignmentInternal $ mconcat
426+ [ linesBefore
427+ , [wrapWithOffsetAndAlignment alignment width 0 $ b <> afterWithCursor]
428+ , linesAfter
429+ ]
430+ flattenLines = concatMap (fmap _wrappedLines_text)
431+ spansBefore = map ((:[]) . Span tag) $ flattenLines linesBefore
432+ spansAfter = map ((:[]) . Span tag) $ flattenLines linesAfter
433+ -- Separate the spans before the cursor into
434+ -- * spans that are on earlier display lines (though on the same logical line), and
435+ -- * spans that are on the same display line
436+
437+ (spansCurrentBefore, spansCurLineBefore) = fromMaybe ([], []) $
438+ initLast $ map ((:[]) . Span tag) $ _wrappedLines_text <$> (wrapWithOffsetAndAlignment alignment width 0 b)
439+ -- Calculate the number of columns on the cursor's display line before the cursor
440+ curLineOffset = spansWidth spansCurLineBefore
441+ -- Check whether the spans on the current display line are long enough that
442+ -- the cursor has to go to the next line
443+ cursorAfterEOL = curLineOffset == width
444+ cursorCharWidth = case T.uncons a of
445+ Nothing -> 1
446+ Just (c, _) -> charWidth c
447+
448+ -- Separate the span after the cursor into
449+ -- * spans that are on the same display line, and
450+ -- * spans that are on later display lines (though on the same logical line)
451+
452+ (spansCurLineAfter, spansCurrentAfter) = fromMaybe ([], []) $
453+ headTail $ case T.uncons a of
454+ Nothing -> [[Span cursorTag " "]]
455+ Just (c, rest) ->
456+ let o = if cursorAfterEOL then cursorCharWidth else curLineOffset + cursorCharWidth
457+ cursor = Span cursorTag (T.singleton c)
458+ in case map ((:[]) . Span tag) $ _wrappedLines_text <$> (wrapWithOffsetAndAlignment alignment width o rest) of
459+ [] -> [[cursor]]
460+ (l:ls) -> (cursor : l) : ls
461+
462+ curLineSpanNormalCase = if cursorAfterEOL
463+ then [ spansCurLineBefore, spansCurLineAfter ]
464+ else [ spansCurLineBefore <> spansCurLineAfter ]
465+
466+ -- for right alignment, we want draw the cursor tag to be on the character just before the logical cursor position
467+ curLineSpan = if alignment == TextAlignment_Right && not cursorAfterEOL
468+ then case reverse spansCurLineBefore of
469+ [] -> curLineSpanNormalCase
470+ (Span _ x):xs -> case spansCurLineAfter of
471+ [] -> error "should not be possible" -- curLineSpanNormalCase
472+ (Span _ y):ys -> [reverse (Span cursorTag x:xs) <> ((Span tag y):ys)]
473+ else curLineSpanNormalCase
474+
475+ cursorY = sum
476+ [ length spansBefore
477+ , length spansCurrentBefore
478+ , if cursorAfterEOL then 1 else 0
479+ ]
480+ -- a little silly to convert back to text but whatever, it works
481+ cursorX = if cursorAfterEOL then 0 else textWidth (mconcat $ fmap (\(Span _ t) -> t) spansCurLineBefore)
482+
483+ in DisplayLines
484+ { _displayLines_spans = concat
485+ [ spansBefore
486+ , spansCurrentBefore
487+ , curLineSpan
488+ , spansCurrentAfter
489+ , spansAfter
490+ ]
491+ , _displayLines_offsetMap = offsets
492+ , _displayLines_cursorPos = (cursorX, cursorY)
493+ }
494+ where
495+ initLast :: [a] -> Maybe ([a], a)
496+ initLast = \case
497+ [] -> Nothing
498+ (x:xs) -> case initLast xs of
499+ Nothing -> Just ([], x)
500+ Just (ys, y) -> Just (x:ys, y)
501+ headTail :: [a] -> Maybe (a, [a])
502+ headTail = \case
503+ [] -> Nothing
504+ x:xs -> Just (x, xs)
505+
506+
507+-- | Move the cursor of the given 'TextZipper' to the logical position indicated
508+-- by the given display line coordinates, using the provided 'DisplayLinesWithAlignment'
509+-- information. If the x coordinate is beyond the end of a line, the cursor is
510+-- moved to the end of the line.
511+goToDisplayLinePosition :: Int -> Int -> DisplayLines tag -> TextZipper -> TextZipper
512+goToDisplayLinePosition x y dl tz =
513+ let offset = Map.lookup y $ _displayLines_offsetMap dl
514+ in case offset of
515+ Nothing -> tz
516+ Just (alignOff,o) ->
517+ let
518+ trueX = max 0 (x - alignOff)
519+ moveRight = case drop y $ _displayLines_spans dl of
520+ [] -> 0
521+ (s:_) -> charIndexAt trueX . stream . mconcat . fmap (\(Span _ t) -> t) $ s
522+ in rightN (o + moveRight) $ top tz
523+
524+-- | Given a width and a 'TextZipper', produce a list of display lines
525+-- (i.e., lines of wrapped text) with special attributes applied to
526+-- certain segments (e.g., the cursor). Additionally, produce the current
527+-- y-coordinate of the cursor and a mapping from display line number to text
528+-- offset
529+displayLines
530+ :: Int -- ^ Width, used for wrapping
531+ -> tag -- ^ Metadata for normal characters
532+ -> tag -- ^ Metadata for the cursor
533+ -> TextZipper -- ^ The text input contents and cursor state
534+ -> DisplayLines tag
535+displayLines = displayLinesWithAlignment TextAlignment_Left
536+
537+-- | Wraps a logical line of text to fit within the given width. The first
538+-- wrapped line is offset by the number of columns provided. Subsequent wrapped
539+-- lines are not.
540+wrapWithOffset
541+ :: Int -- ^ Maximum width
542+ -> Int -- ^ Offset for first line
543+ -> Text -- ^ Text to be wrapped
544+ -> [Text]
545+wrapWithOffset maxWidth n xs = _wrappedLines_text <$> wrapWithOffsetAndAlignment TextAlignment_Left maxWidth n xs
src/Reflex/Class/Switchable.hsdeleted−27
@@ -1,27 +0,0 @@
1−{-|
2−Module: Reflex.Class.Switchable
3−Description: A class for things that can be switched on the firing of an event
4−-}
5−{-# Language UndecidableInstances #-}
6−module Reflex.Class.Switchable where
7−
8−import Control.Monad
9−import Reflex
10−
11−-- | Class representing things that can be switched when the provided event occurs
12−class Reflex t => Switchable t w | w -> t where
13− switching :: MonadHold t m => w -> Event t w -> m w
14−
15−instance Reflex t => Switchable t (Event t a) where
16− switching = switchHold
17−
18−instance Reflex t => Switchable t (Dynamic t a) where
19− switching a e = fmap join $ holdDyn a e
20−
21−instance Reflex t => Switchable t (Behavior t a) where
22− switching = switcher
23−
24−instance (Reflex t, Switchable t a, Switchable t b) => Switchable t (a, b) where
25− switching (a, b) e = (,)
26− <$> switching a (fmap fst e)
27− <*> switching b (fmap snd e)
src/Reflex/Vty.hs+6
@@ -14,15 +14,21 @@ module Reflex.Vty
1414 ( module Reflex
1515 , module Reflex.Vty.Host
1616 , module Reflex.Vty.Widget
17+ , module Reflex.Vty.Widget.Box
1718 , module Reflex.Vty.Widget.Input
1819 , module Reflex.Vty.Widget.Layout
20+ , module Reflex.Vty.Widget.Split
21+ , module Reflex.Vty.Widget.Text
1922 , module Control.Monad.NodeId
2023 ) where
2124
2225import Reflex
2326import Reflex.Vty.Host
2427import Reflex.Vty.Widget
28+import Reflex.Vty.Widget.Box
2529import Reflex.Vty.Widget.Input
2630import Reflex.Vty.Widget.Layout
31+import Reflex.Vty.Widget.Split
32+import Reflex.Vty.Widget.Text
2733
2834import Control.Monad.NodeId
src/Reflex/Vty/Widget.hs+465−610
@@ -4,69 +4,18 @@ Description: Basic set of widgets and building blocks for reflex-vty application
44-}
55{-# Language UndecidableInstances #-}
66
7−module Reflex.Vty.Widget
8− ( VtyWidgetCtx(..)
9− , VtyWidget(..)
10− , VtyWidgetOut(..)
11− , ImageWriter(..)
12− , runVtyWidget
13− , mainWidget
14− , mainWidgetWithHandle
15− , HasDisplaySize(..)
16− , HasFocus(..)
17− , HasVtyInput(..)
18− , HasVtyWidgetCtx(..)
19− , Region(..)
20− , regionSize
21− , regionBlankImage
22− , Drag(..)
23− , drag
24− , MouseDown(..)
25− , MouseUp(..)
26− , mouseDown
27− , mouseUp
28− , ScrollDirection(..)
29− , mouseScroll
30− , pane
31− , splitV
32− , splitH
33− , splitVDrag
34− , boxTitle
35− , box
36− , boxStatic
37− , RichTextConfig(..)
38− , richText
39− , text
40− , scrollableText
41− , display
42− , BoxStyle(..)
43− , hyphenBoxStyle
44− , singleBoxStyle
45− , roundedBoxStyle
46− , thickBoxStyle
47− , doubleBoxStyle
48− , fill
49− , hRule
50− , KeyCombo
51− , key
52− , keys
53− , keyCombo
54− , keyCombos
55− , blank
56− ) where
7+module Reflex.Vty.Widget where
578
589import Control.Applicative (liftA2)
5910import Control.Monad.Fix (MonadFix)
6011import Control.Monad.IO.Class (MonadIO)
12+import Control.Monad.Morph
6113import Control.Monad.NodeId
62−import Control.Monad.Reader (ReaderT, ask, asks, local, runReaderT)
14+import Control.Monad.Reader (ReaderT, ask, local, runReaderT)
15+import Control.Monad.Ref
6316import Control.Monad.Trans (MonadTrans, lift)
64−import Data.Default (Default(..))
6517import Data.Set (Set)
6618import qualified Data.Set as Set
67−import Data.Text (Text)
68−import qualified Data.Text as T
69−import qualified Data.Text.Zipper as TZ
7019import Graphics.Vty (Image)
7120import qualified Graphics.Vty as V
7221import Reflex
@@ -74,87 +23,20 @@ import Reflex.Class ()
7423import Reflex.Host.Class (MonadReflexCreateTrigger)
7524import Reflex.Vty.Host
7625
77−-- | The context within which a 'VtyWidget' runs
78−data VtyWidgetCtx t = VtyWidgetCtx
79− { _vtyWidgetCtx_width :: Dynamic t Int
80− -- ^ The width of the region allocated to the widget.
81− , _vtyWidgetCtx_height :: Dynamic t Int
82− -- ^ The height of the region allocated to the widget.
83− , _vtyWidgetCtx_focus :: Dynamic t Bool
84− -- ^ Whether the widget should behave as if it has focus for keyboard input.
85− , _vtyWidgetCtx_input :: Event t VtyEvent
86− -- ^ User input events that the widget's parent chooses to share. These will generally
87− -- be filtered for relevance:
88− -- * Keyboard inputs are restricted to focused widgets
89− -- * Mouse inputs are restricted to the region in which the widget resides and are
90− -- translated into its internal coordinates.
91− }
26+-- * Running a vty application
9227
93−-- | The output of a 'VtyWidget'
94−data VtyWidgetOut t = VtyWidgetOut
95− { _vtyWidgetOut_shutdown :: Event t ()
96− }
97−
98−instance (Adjustable t m, MonadHold t m, Reflex t) => Adjustable t (VtyWidget t m) where
99− runWithReplace a0 a' = VtyWidget $ runWithReplace (unVtyWidget a0) $ fmap unVtyWidget a'
100− traverseIntMapWithKeyWithAdjust f dm0 dm' = VtyWidget $
101− traverseIntMapWithKeyWithAdjust (\k v -> unVtyWidget (f k v)) dm0 dm'
102− traverseDMapWithKeyWithAdjust f dm0 dm' = VtyWidget $ do
103− traverseDMapWithKeyWithAdjust (\k v -> unVtyWidget (f k v)) dm0 dm'
104− traverseDMapWithKeyWithAdjustWithMove f dm0 dm' = VtyWidget $ do
105− traverseDMapWithKeyWithAdjustWithMove (\k v -> unVtyWidget (f k v)) dm0 dm'
106−
107−-- | A widget that can read its context and produce image output
108−newtype VtyWidget t m a = VtyWidget
109− { unVtyWidget :: BehaviorWriterT t [Image] (ReaderT (VtyWidgetCtx t) m) a
110− } deriving
111− ( Functor
112− , Applicative
113− , Monad
114− , MonadSample t
115− , MonadHold t
116− , MonadFix
117− , NotReady t
118− , ImageWriter t
119− , PostBuild t
120− , TriggerEvent t
121− , MonadReflexCreateTrigger t
122− , MonadIO
123− )
124−
125−deriving instance PerformEvent t m => PerformEvent t (VtyWidget t m)
126−instance MonadTrans (VtyWidget t) where
127− lift f = VtyWidget $ lift $ lift f
128−
129−instance MonadNodeId m => MonadNodeId (VtyWidget t m) where
130− getNextNodeId = VtyWidget $ do
131− lift $ lift getNextNodeId
132−
133−class HasVtyWidgetCtx t m | m -> t where
134− askCtx :: m (VtyWidgetCtx t)
135− default askCtx :: (f m' ~ m, Monad m', MonadTrans f, HasVtyWidgetCtx t m') => m (VtyWidgetCtx t)
136− askCtx = lift askCtx
137− localCtx :: (VtyWidgetCtx t -> VtyWidgetCtx t) -> (Behavior t [Image] -> Behavior t [Image]) -> m a -> m a
138−
139−instance (Monad m, Reflex t) => HasVtyWidgetCtx t (VtyWidget t m) where
140− askCtx = VtyWidget $ lift ask
141− localCtx f g (VtyWidget x) = VtyWidget $ do
142− (a, images) :: (a, Behavior t [Image]) <- lift $ local f $ runBehaviorWriterT x
143− tellImages $ g images
144− pure a
145−
146−-- | Runs a 'VtyWidget' with a given context
147−runVtyWidget
148− :: (Reflex t, MonadNodeId m)
149− => VtyWidgetCtx t
150− -> VtyWidget t m a
151− -> m (a, Behavior t [Image])
152−runVtyWidget ctx w = runReaderT (runBehaviorWriterT (unVtyWidget w)) ctx
153−
154−-- | 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
15529mainWidgetWithHandle
15630 :: V.Vty
157− -> (forall t m. (MonadVtyApp t m, MonadNodeId m) => VtyWidget t m (Event t ()))
31+ -> (forall t m.
32+ ( MonadVtyApp t m
33+ , HasImageWriter t m
34+ , MonadNodeId m
35+ , HasDisplayRegion t m
36+ , HasFocusReader t m
37+ , HasInput t m
38+ , HasTheme t m
39+ ) => m (Event t ()))
15840 -> IO ()
15941mainWidgetWithHandle vty child =
16042 runVtyAppWithHandle vty $ \dr0 inp -> do
@@ -164,77 +46,178 @@ mainWidgetWithHandle vty child =
16446 let inp' = fforMaybe inp $ \case
16547 V.EvResize {} -> Nothing
16648 x -> Just x
167− let ctx = VtyWidgetCtx
168− { _vtyWidgetCtx_width = fmap fst size
169− , _vtyWidgetCtx_height = fmap snd size
170− , _vtyWidgetCtx_input = inp'
171− , _vtyWidgetCtx_focus = constDyn True
172− }
173− (shutdown, images) <- runNodeIdT $ runVtyWidget ctx $ do
174− tellImages . ffor (current size) $ \(w, h) -> [V.charFill V.defAttr ' ' w h]
175− child
49+ (shutdown, images) <- runThemeReader (constant V.defAttr) $
50+ runFocusReader (pure True) $
51+ runDisplayRegion (fmap (\(w, h) -> Region 0 0 w h) size) $
52+ runImageWriter $
53+ runNodeIdT $
54+ runInput inp' $ do
55+ tellImages . ffor (current size) $ \(w, h) -> [V.charFill V.defAttr ' ' w h]
56+ child
17657 return $ VtyResult
17758 { _vtyResult_picture = fmap (V.picForLayers . reverse) images
17859 , _vtyResult_shutdown = shutdown
17960 }
18061
62+-- | The output of a vty widget
63+data VtyWidgetOut t = VtyWidgetOut
64+ { _vtyWidgetOut_shutdown :: Event t ()
65+ }
66+
18167-- | Like 'mainWidgetWithHandle', but uses a default vty configuration
18268mainWidget
183− :: (forall t m. (MonadVtyApp t m, MonadNodeId m) => VtyWidget t m (Event t ()))
69+ :: (forall t m.
70+ ( MonadVtyApp t m
71+ , HasImageWriter t m
72+ , MonadNodeId m
73+ , HasDisplayRegion t m
74+ , HasFocusReader t m
75+ , HasTheme t m
76+ , HasInput t m
77+ ) => m (Event t ()))
18478 -> IO ()
18579mainWidget child = do
18680 vty <- getDefaultVty
18781 mainWidgetWithHandle vty child
18882
189−-- | A class for things that know their own display size dimensions
190−class (Reflex t, Monad m) => HasDisplaySize t m | m -> t where
191− -- | Retrieve the display width (columns)
192− displayWidth :: m (Dynamic t Int)
193− default displayWidth :: (f m' ~ m, MonadTrans f, HasDisplaySize t m') => m (Dynamic t Int)
194− displayWidth = lift displayWidth
195− -- | Retrieve the display height (rows)
196− displayHeight :: m (Dynamic t Int)
197− default displayHeight :: (f m' ~ m, MonadTrans f, HasDisplaySize t m') => m (Dynamic t Int)
198− displayHeight = lift displayHeight
199−
200−instance (Reflex t, Monad m) => HasDisplaySize t (VtyWidget t m) where
201− displayWidth = VtyWidget . lift $ asks _vtyWidgetCtx_width
202− displayHeight = VtyWidget . lift $ asks _vtyWidgetCtx_height
203−
204−instance HasDisplaySize t m => HasDisplaySize t (ReaderT x m)
205−instance HasDisplaySize t m => HasDisplaySize t (BehaviorWriterT t x m)
206−instance HasDisplaySize t m => HasDisplaySize t (DynamicWriterT t x m)
207−instance HasDisplaySize t m => HasDisplaySize t (EventWriterT t x m)
208−
209−instance HasDisplaySize t m => HasDisplaySize t (NodeIdT m)
83+-- * Input Events
21084
21185-- | A class for things that can receive vty events as input
212−class HasVtyInput t m | m -> t where
86+class HasInput t m | m -> t where
21387 input :: m (Event t VtyEvent)
214− default input :: (f m' ~ m, Monad m', MonadTrans f, HasVtyInput t m') => m (Event t VtyEvent)
88+ default input :: (f m' ~ m, Monad m', MonadTrans f, HasInput t m') => m (Event t VtyEvent)
21589 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+ )
216113
217−instance (Reflex t, Monad m) => HasVtyInput t (VtyWidget t m) where
218− input = VtyWidget . lift $ asks _vtyWidgetCtx_input
114+instance (Adjustable t m, MonadHold t m, Reflex t) => Adjustable t (Input t m) where
115+ runWithReplace a0 a' = Input $ runWithReplace (unInput a0) $ fmap unInput a'
116+ traverseIntMapWithKeyWithAdjust f dm0 dm' = Input $
117+ traverseIntMapWithKeyWithAdjust (\k v -> unInput (f k v)) dm0 dm'
118+ traverseDMapWithKeyWithAdjust f dm0 dm' = Input $ do
119+ traverseDMapWithKeyWithAdjust (\k v -> unInput (f k v)) dm0 dm'
120+ traverseDMapWithKeyWithAdjustWithMove f dm0 dm' = Input $ do
121+ traverseDMapWithKeyWithAdjustWithMove (\k v -> unInput (f k v)) dm0 dm'
122+
123+deriving instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (Input t m)
124+deriving instance NotReady t m => NotReady t (Input t m)
125+deriving instance PerformEvent t m => PerformEvent t (Input t m)
126+deriving instance PostBuild t m => PostBuild t (Input t m)
127+deriving instance TriggerEvent t m => TriggerEvent t (Input t m)
128+instance HasImageWriter t m => HasImageWriter t (Input t m)
129+instance HasDisplayRegion t m => HasDisplayRegion t (Input t m)
130+instance HasFocusReader t m => HasFocusReader t (Input t m)
131+
132+instance MonadTrans (Input t) where
133+ lift f = Input $ lift f
134+
135+instance MFunctor (Input t) where
136+ hoist f = Input . hoist f . unInput
137+
138+instance MonadNodeId m => MonadNodeId (Input t m)
139+
140+-- | Runs an 'Input' with a given context
141+runInput
142+ :: Reflex t
143+ => Event t VtyEvent
144+ -> Input t m a
145+ -> m a
146+runInput e w = runReaderT (unInput w) e
219147
220−-- | A class for things that can dynamically gain and lose focus
221−class HasFocus t m | m -> t where
222− focus :: m (Dynamic t Bool)
223− default focus :: (f m' ~ m, Monad m', MonadTrans f, HasFocus t m') => m (Dynamic t Bool)
224− focus = lift focus
148+-- ** Filtering input
225149
226−instance (Reflex t, Monad m) => HasFocus t (VtyWidget t m) where
227− focus = VtyWidget . lift $ asks _vtyWidgetCtx_focus
150+-- | Type synonym for a key and modifier combination
151+type KeyCombo = (V.Key, [V.Modifier])
228152
229−-- | A class for widgets that can produce images to draw to the display
230−class (Reflex t, Monad m) => ImageWriter t m | m -> t where
231− -- | Send images upstream for rendering
232− tellImages :: Behavior t [Image] -> m ()
233− default tellImages :: (f m' ~ m, Monad m', MonadTrans f, ImageWriter t m') => Behavior t [Image] -> m ()
234− tellImages = lift . tellImages
153+-- | Emits an event that fires on a particular key press (without modifiers)
154+key :: (Monad m, Reflex t, HasInput t m) => V.Key -> m (Event t KeyCombo)
155+key = keyCombos . Set.singleton . (,[])
156+
157+-- | Emits an event that fires on particular key presses (without modifiers)
158+keys :: (Monad m, Reflex t, HasInput t m) => [V.Key] -> m (Event t KeyCombo)
159+keys = keyCombos . Set.fromList . fmap (,[])
160+
161+-- | Emit an event that fires whenever the provided key combination occurs
162+keyCombo
163+ :: (Reflex t, Monad m, HasInput t m)
164+ => KeyCombo
165+ -> m (Event t KeyCombo)
166+keyCombo = keyCombos . Set.singleton
235167
236−instance (Monad m, Reflex t) => ImageWriter t (BehaviorWriterT t [Image] m) where
237− tellImages = tellBehavior
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
238221
239222-- | A chunk of the display area
240223data Region = Region
@@ -245,14 +228,293 @@ data Region = Region
245228 }
246229 deriving (Show, Read, Eq, Ord)
247230
231+-- | A region that occupies no space.
232+nilRegion :: Region
233+nilRegion = Region 0 0 0 0
234+
248235-- | The width and height of a 'Region'
249236regionSize :: Region -> (Int, Int)
250237regionSize (Region _ _ w h) = (w, h)
251238
252239-- | Produces an 'Image' that fills a region with space characters
253−regionBlankImage :: Region -> Image
254−regionBlankImage r@(Region _ _ width height) =
255− withinImage r $ V.charFill V.defAttr ' ' width height
240+regionBlankImage :: V.Attr -> Region -> Image
241+regionBlankImage attr r@(Region _ _ width height) =
242+ withinImage r $ V.charFill attr ' ' width height
243+
244+-- | A class for things that know their own display size dimensions
245+class (Reflex t, Monad m) => HasDisplayRegion t m | m -> t where
246+ -- | Retrieve the display region
247+ askRegion :: m (Dynamic t Region)
248+ default askRegion :: (f m' ~ m, MonadTrans f, HasDisplayRegion t m') => m (Dynamic t Region)
249+ askRegion = lift askRegion
250+ -- | Run an action in a local region, by applying a transformation to the region
251+ localRegion :: (Dynamic t Region -> Dynamic t Region) -> m a -> m a
252+ default localRegion :: (f m' ~ m, Monad m', MFunctor f, HasDisplayRegion t m') => (Dynamic t Region -> Dynamic t Region) -> m a -> m a
253+ localRegion f = hoist (localRegion f)
254+
255+-- | Retrieve the display width
256+displayWidth :: HasDisplayRegion t m => m (Dynamic t Int)
257+displayWidth = fmap _region_width <$> askRegion
258+
259+-- | Retrieve the display height
260+displayHeight :: HasDisplayRegion t m => m (Dynamic t Int)
261+displayHeight = fmap _region_height <$> askRegion
262+
263+instance HasDisplayRegion t m => HasDisplayRegion t (ReaderT x m)
264+instance HasDisplayRegion t m => HasDisplayRegion t (BehaviorWriterT t x m)
265+instance HasDisplayRegion t m => HasDisplayRegion t (DynamicWriterT t x m)
266+instance HasDisplayRegion t m => HasDisplayRegion t (EventWriterT t x m)
267+instance HasDisplayRegion t m => HasDisplayRegion t (NodeIdT m)
268+
269+-- | A widget that has access to a particular region of the vty display
270+newtype DisplayRegion t m a = DisplayRegion
271+ { unDisplayRegion :: ReaderT (Dynamic t Region) m a }
272+ deriving
273+ ( Functor
274+ , Applicative
275+ , Monad
276+ , MonadFix
277+ , MonadHold t
278+ , MonadIO
279+ , MonadRef
280+ , MonadSample t
281+ )
282+
283+instance (Monad m, Reflex t) => HasDisplayRegion t (DisplayRegion t m) where
284+ askRegion = DisplayRegion ask
285+ localRegion f = DisplayRegion . local f . unDisplayRegion
286+
287+deriving instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (DisplayRegion t m)
288+deriving instance NotReady t m => NotReady t (DisplayRegion t m)
289+deriving instance PerformEvent t m => PerformEvent t (DisplayRegion t m)
290+deriving instance PostBuild t m => PostBuild t (DisplayRegion t m)
291+deriving instance TriggerEvent t m => TriggerEvent t (DisplayRegion t m)
292+instance HasImageWriter t m => HasImageWriter t (DisplayRegion t m)
293+instance HasFocusReader t m => HasFocusReader t (DisplayRegion t m)
294+
295+instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (DisplayRegion t m) where
296+ runWithReplace (DisplayRegion a) e = DisplayRegion $ runWithReplace a $ fmap unDisplayRegion e
297+ traverseIntMapWithKeyWithAdjust f m e = DisplayRegion $ traverseIntMapWithKeyWithAdjust (\k v -> unDisplayRegion $ f k v) m e
298+ traverseDMapWithKeyWithAdjust f m e = DisplayRegion $ traverseDMapWithKeyWithAdjust (\k v -> unDisplayRegion $ f k v) m e
299+ traverseDMapWithKeyWithAdjustWithMove f m e = DisplayRegion $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unDisplayRegion $ f k v) m e
300+
301+instance MonadTrans (DisplayRegion t) where
302+ lift = DisplayRegion . lift
303+
304+instance MFunctor (DisplayRegion t) where
305+ hoist f = DisplayRegion . hoist f . unDisplayRegion
306+
307+instance MonadNodeId m => MonadNodeId (DisplayRegion t m)
308+
309+-- | Run a 'DisplayRegion' action with a given 'Region'
310+runDisplayRegion
311+ :: (Reflex t, Monad m)
312+ => Dynamic t Region
313+ -> DisplayRegion t m a
314+ -> m a
315+runDisplayRegion r = flip runReaderT r . unDisplayRegion
316+
317+-- * Getting focus state
318+
319+-- | A class for things that can dynamically gain and lose focus
320+class (Reflex t, Monad m) => HasFocusReader t m | m -> t where
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+ )
347+
348+instance (Monad m, Reflex t) => HasFocusReader t (FocusReader t m) where
349+ focus = FocusReader ask
350+ localFocus f = FocusReader . local f . unFocusReader
351+
352+deriving instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (FocusReader t m)
353+deriving instance NotReady t m => NotReady t (FocusReader t m)
354+deriving instance PerformEvent t m => PerformEvent t (FocusReader t m)
355+deriving instance PostBuild t m => PostBuild t (FocusReader t m)
356+deriving instance TriggerEvent t m => TriggerEvent t (FocusReader t m)
357+instance HasImageWriter t m => HasImageWriter t (FocusReader t m)
358+
359+instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (FocusReader t m) where
360+ runWithReplace (FocusReader a) e = FocusReader $ runWithReplace a $ fmap unFocusReader e
361+ traverseIntMapWithKeyWithAdjust f m e = FocusReader $ traverseIntMapWithKeyWithAdjust (\k v -> unFocusReader $ f k v) m e
362+ traverseDMapWithKeyWithAdjust f m e = FocusReader $ traverseDMapWithKeyWithAdjust (\k v -> unFocusReader $ f k v) m e
363+ traverseDMapWithKeyWithAdjustWithMove f m e = FocusReader $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unFocusReader $ f k v) m e
364+
365+instance MonadTrans (FocusReader t) where
366+ lift = FocusReader . lift
367+
368+instance MFunctor (FocusReader t) where
369+ hoist f = FocusReader . hoist f . unFocusReader
370+
371+instance MonadNodeId m => MonadNodeId (FocusReader t m)
372+
373+-- | Run a 'FocusReader' action with the given focus value
374+runFocusReader
375+ :: (Reflex t, Monad m)
376+ => Dynamic t Bool
377+ -> FocusReader t m a
378+ -> m a
379+runFocusReader b = flip runReaderT b . unFocusReader
380+
381+-- * "Image" output
382+
383+-- | A class for widgets that can produce images to draw to the display
384+class (Reflex t, Monad m) => HasImageWriter t m | m -> t where
385+ -- | Send images upstream for rendering
386+ tellImages :: Behavior t [Image] -> m ()
387+ default tellImages :: (f m' ~ m, Monad m', MonadTrans f, HasImageWriter t m') => Behavior t [Image] -> m ()
388+ tellImages = lift . tellImages
389+ -- | Apply a transformation to the images produced by the child actions
390+ mapImages :: (Behavior t [Image] -> Behavior t [Image]) -> m a -> m a
391+ default mapImages :: (f m' ~ m, Monad m', MFunctor f, HasImageWriter t m') => (Behavior t [Image] -> Behavior t [Image]) -> m a -> m a
392+ mapImages f = hoist (mapImages f)
393+
394+-- | A widget that can produce images to draw onto the display
395+newtype ImageWriter t m a = ImageWriter
396+ { unImageWriter :: BehaviorWriterT t [Image] m a }
397+ deriving
398+ ( Functor
399+ , Applicative
400+ , Monad
401+ , MonadFix
402+ , MonadHold t
403+ , MonadIO
404+ , MonadRef
405+ , MonadReflexCreateTrigger t
406+ , MonadSample t
407+ , NotReady t
408+ , PerformEvent t
409+ , PostBuild t
410+ , TriggerEvent t
411+ )
412+
413+instance MonadTrans (ImageWriter t) where
414+ lift = ImageWriter . lift
415+
416+instance MFunctor (ImageWriter t) where
417+ hoist f = ImageWriter . (hoist f) . unImageWriter
418+
419+instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (ImageWriter t m) where
420+ runWithReplace (ImageWriter a) e = ImageWriter $ runWithReplace a $ fmap unImageWriter e
421+ traverseIntMapWithKeyWithAdjust f m e = ImageWriter $ traverseIntMapWithKeyWithAdjust (\k v -> unImageWriter $ f k v) m e
422+ traverseDMapWithKeyWithAdjust f m e = ImageWriter $ traverseDMapWithKeyWithAdjust (\k v -> unImageWriter $ f k v) m e
423+ traverseDMapWithKeyWithAdjustWithMove f m e = ImageWriter $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unImageWriter $ f k v) m e
424+
425+instance HasImageWriter t m => HasImageWriter t (ReaderT x m)
426+instance HasImageWriter t m => HasImageWriter t (BehaviorWriterT t x m)
427+instance HasImageWriter t m => HasImageWriter t (DynamicWriterT t x m)
428+instance HasImageWriter t m => HasImageWriter t (EventWriterT t x m)
429+instance HasImageWriter t m => HasImageWriter t (NodeIdT m)
430+
431+instance (Monad m, Reflex t) => HasImageWriter t (ImageWriter t m) where
432+ tellImages = ImageWriter . tellBehavior
433+ mapImages f (ImageWriter x) = ImageWriter $ do
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
443+ :: (Reflex t, Monad m)
444+ => ImageWriter t m a
445+ -> m (a, Behavior t [Image])
446+runImageWriter = runBehaviorWriterT . unImageWriter
447+
448+-- * Theming
449+
450+-- | A class for things that can be visually styled
451+class (Reflex t, Monad m) => HasTheme t m | m -> t where
452+ theme :: m (Behavior t V.Attr)
453+ default theme :: (f m' ~ m, Monad m', MonadTrans f, HasTheme t m') => m (Behavior t V.Attr)
454+ theme = lift theme
455+ localTheme :: (Behavior t V.Attr -> Behavior t V.Attr) -> m a -> m a
456+ default localTheme :: (f m' ~ m, Monad m', MFunctor f, HasTheme t m') => (Behavior t V.Attr -> Behavior t V.Attr) -> m a -> m a
457+ localTheme f = hoist (localTheme f)
458+
459+instance HasTheme t m => HasTheme t (ReaderT x m)
460+instance HasTheme t m => HasTheme t (BehaviorWriterT t x m)
461+instance HasTheme t m => HasTheme t (DynamicWriterT t x m)
462+instance HasTheme t m => HasTheme t (EventWriterT t x m)
463+instance HasTheme t m => HasTheme t (NodeIdT m)
464+instance HasTheme t m => HasTheme t (Input t m)
465+instance HasTheme t m => HasTheme t (ImageWriter t m)
466+instance HasTheme t m => HasTheme t (DisplayRegion t m)
467+instance HasTheme t m => HasTheme t (FocusReader t m)
468+
469+-- | A widget that has access to theme information
470+newtype ThemeReader t m a = ThemeReader
471+ { unThemeReader :: ReaderT (Behavior t V.Attr) m a }
472+ deriving
473+ ( Functor
474+ , Applicative
475+ , Monad
476+ , MonadFix
477+ , MonadHold t
478+ , MonadIO
479+ , MonadRef
480+ , MonadSample t
481+ )
482+
483+instance (Monad m, Reflex t) => HasTheme t (ThemeReader t m) where
484+ theme = ThemeReader ask
485+ localTheme f = ThemeReader . local f . unThemeReader
486+
487+deriving instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (ThemeReader t m)
488+deriving instance NotReady t m => NotReady t (ThemeReader t m)
489+deriving instance PerformEvent t m => PerformEvent t (ThemeReader t m)
490+deriving instance PostBuild t m => PostBuild t (ThemeReader t m)
491+deriving instance TriggerEvent t m => TriggerEvent t (ThemeReader t m)
492+instance HasImageWriter t m => HasImageWriter t (ThemeReader t m)
493+
494+instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (ThemeReader t m) where
495+ runWithReplace (ThemeReader a) e = ThemeReader $ runWithReplace a $ fmap unThemeReader e
496+ traverseIntMapWithKeyWithAdjust f m e = ThemeReader $ traverseIntMapWithKeyWithAdjust (\k v -> unThemeReader $ f k v) m e
497+ traverseDMapWithKeyWithAdjust f m e = ThemeReader $ traverseDMapWithKeyWithAdjust (\k v -> unThemeReader $ f k v) m e
498+ traverseDMapWithKeyWithAdjustWithMove f m e = ThemeReader $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unThemeReader $ f k v) m e
499+
500+instance MonadTrans (ThemeReader t) where
501+ lift = ThemeReader . lift
502+
503+instance MFunctor (ThemeReader t) where
504+ hoist f = ThemeReader . hoist f . unThemeReader
505+
506+instance MonadNodeId m => MonadNodeId (ThemeReader t m)
507+
508+-- | Run a 'ThemeReader' action with the given focus value
509+runThemeReader
510+ :: (Reflex t, Monad m)
511+ => Behavior t V.Attr
512+ -> ThemeReader t m a
513+ -> m a
514+runThemeReader b = flip runReaderT b . unThemeReader
515+
516+
517+-- ** Manipulating images
256518
257519-- | Translates and crops an 'Image' so that it is contained by
258520-- the given 'Region'.
@@ -264,7 +526,17 @@ withinImage (Region left top width height)
264526 | width < 0 || height < 0 = withinImage (Region left top 0 0)
265527 | otherwise = V.translate left top . V.crop width height
266528
267−-- | Low-level widget combinator that runs a child 'VtyWidget' within
529+-- | Crop a behavior of images to a behavior of regions. See 'withinImage'.
530+imagesInRegion
531+ :: Reflex t
532+ => Behavior t Region
533+ -> Behavior t [Image]
534+ -> Behavior t [Image]
535+imagesInRegion reg = liftA2 (\r is -> map (withinImage r) is) reg
536+
537+-- * Running sub-widgets
538+
539+-- | Low-level widget combinator that runs a child widget within
268540-- a given region and context. This widget filters and modifies the input
269541-- that the child widget receives such that:
270542-- * unfocused widgets receive no key events
@@ -272,434 +544,17 @@ withinImage (Region left top width height)
272544-- * mouse inputs inside the region have their coordinates translated such
273545-- that (0,0) is the top-left corner of the region
274546pane
275− :: (Reflex t, Monad m, MonadNodeId m, HasVtyWidgetCtx t m)
547+ :: (Reflex t, Monad m, HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
276548 => Dynamic t Region
277549 -> Dynamic t Bool -- ^ Whether the widget should be focused when the parent is.
278550 -> m a
279551 -> m a
280−pane dr foc child = do
281− let reg = current dr
282− let subContext ctx = VtyWidgetCtx
283− { _vtyWidgetCtx_input = fmapMaybe id $
284− attachWith (\(r,f) e -> filterInput r f e)
285− (liftA2 (,) reg (current foc))
286− (_vtyWidgetCtx_input ctx)
287− , _vtyWidgetCtx_focus = liftA2 (&&) (_vtyWidgetCtx_focus ctx) foc
288− , _vtyWidgetCtx_width = _region_width <$> dr
289− , _vtyWidgetCtx_height = _region_height <$> dr
290− }
291− let imagesWithinRegion images = liftA2 (\is r -> map (withinImage r) is) images reg
292− localCtx subContext imagesWithinRegion child
293− where
294− filterInput :: Region -> Bool -> VtyEvent -> Maybe VtyEvent
295− filterInput (Region l t w h) focused e = case e of
296− V.EvKey _ _ | not focused -> Nothing
297− V.EvMouseDown x y btn m -> mouse (\u v -> V.EvMouseDown u v btn m) x y
298− V.EvMouseUp x y btn -> mouse (\u v -> V.EvMouseUp u v btn) x y
299− _ -> Just e
300− where
301− mouse con x y
302− | or [ x < l
303− , y < t
304− , x >= l + w
305− , y >= t + h ] = Nothing
306− | otherwise =
307− Just (con (x - l) (y - t))
308−
309−-- | Information about a drag operation
310−data Drag = Drag
311− { _drag_from :: (Int, Int) -- ^ Where the drag began
312− , _drag_to :: (Int, Int) -- ^ Where the mouse currently is
313− , _drag_button :: V.Button -- ^ Which mouse button is dragging
314− , _drag_modifiers :: [V.Modifier] -- ^ What modifiers are held
315− , _drag_end :: Bool -- ^ Whether the drag ended (the mouse button was released)
316− }
317− deriving (Eq, Ord, Show)
318−
319−-- | Converts raw vty mouse drag events into an event stream of 'Drag's
320−drag
321− :: (Reflex t, MonadFix m, MonadHold t m, HasVtyInput t m)
322− => V.Button
323− -> m (Event t Drag)
324−drag btn = do
325− inp <- input
326− let f :: Maybe Drag -> V.Event -> Maybe Drag
327− f Nothing = \case
328− V.EvMouseDown x y btn' mods
329− | btn == btn' -> Just $ Drag (x,y) (x,y) btn' mods False
330− | otherwise -> Nothing
331− _ -> Nothing
332− f (Just (Drag from _ _ mods end)) = \case
333− V.EvMouseDown x y btn' mods'
334− | end && btn == btn' -> Just $ Drag (x,y) (x,y) btn' mods' False
335− | btn == btn' -> Just $ Drag from (x,y) btn mods' False
336− | otherwise -> Nothing -- Ignore other buttons.
337− V.EvMouseUp x y (Just btn')
338− | end -> Nothing
339− | btn == btn' -> Just $ Drag from (x,y) btn mods True
340− | otherwise -> Nothing
341− V.EvMouseUp x y Nothing -- Terminal doesn't specify mouse up button,
342− -- assume it's the right one.
343− | end -> Nothing
344− | otherwise -> Just $ Drag from (x,y) btn mods True
345− _ -> Nothing
346− rec let newDrag = attachWithMaybe f (current dragD) inp
347− dragD <- holdDyn Nothing $ Just <$> newDrag
348− return (fmapMaybe id $ updated dragD)
349−
350−-- | Mouse down events for a particular mouse button
351−mouseDown
352− :: (Reflex t, Monad m, HasVtyInput t m)
353− => V.Button
354− -> m (Event t MouseDown)
355−mouseDown btn = do
356− i <- input
357− return $ fforMaybe i $ \case
358− V.EvMouseDown x y btn' mods -> if btn == btn'
359− then Just $ MouseDown btn' (x, y) mods
360− else Nothing
361− _ -> Nothing
362−
363−-- | Mouse up events for a particular mouse button
364−mouseUp
365− :: (Reflex t, Monad m, HasVtyInput t m)
366− => m (Event t MouseUp)
367−mouseUp = do
368− i <- input
369− return $ fforMaybe i $ \case
370− V.EvMouseUp x y btn' -> Just $ MouseUp btn' (x, y)
371− _ -> Nothing
372−
373−-- | Information about a mouse down event
374−data MouseDown = MouseDown
375− { _mouseDown_button :: V.Button
376− , _mouseDown_coordinates :: (Int, Int)
377− , _mouseDown_modifiers :: [V.Modifier]
378− }
379− deriving (Eq, Ord, Show)
380−
381−-- | Information about a mouse up event
382−data MouseUp = MouseUp
383− { _mouseUp_button :: Maybe V.Button
384− , _mouseUp_coordinates :: (Int, Int)
385− }
386− deriving (Eq, Ord, Show)
387−
388−-- | Mouse scroll direction
389−data ScrollDirection = ScrollDirection_Up | ScrollDirection_Down
390− deriving (Eq, Ord, Show)
391−
392−-- | Produce an event that fires when the mouse wheel is scrolled
393−mouseScroll
394− :: (Reflex t, Monad m, HasVtyInput t m)
395− => m (Event t ScrollDirection)
396−mouseScroll = do
397− up <- mouseDown V.BScrollUp
398− down <- mouseDown V.BScrollDown
399− return $ leftmost
400− [ ScrollDirection_Up <$ up
401− , ScrollDirection_Down <$ down
402− ]
403−
404−-- | Type synonym for a key and modifier combination
405−type KeyCombo = (V.Key, [V.Modifier])
406−
407−-- | Emits an event that fires on a particular key press (without modifiers)
408−key :: (Monad m, Reflex t, HasVtyInput t m) => V.Key -> m (Event t KeyCombo)
409−key = keyCombos . Set.singleton . (,[])
410−
411−-- | Emits an event that fires on particular key presses (without modifiers)
412−keys :: (Monad m, Reflex t, HasVtyInput t m) => [V.Key] -> m (Event t KeyCombo)
413−keys = keyCombos . Set.fromList . fmap (,[])
414−
415−-- | Emit an event that fires whenever the provided key combination occurs
416−keyCombo
417− :: (Reflex t, Monad m, HasVtyInput t m)
418− => KeyCombo
419− -> m (Event t KeyCombo)
420−keyCombo = keyCombos . Set.singleton
421−
422−-- | Emit an event that fires whenever any of the provided key combinations occur
423−keyCombos
424− :: (Reflex t, Monad m, HasVtyInput t m)
425− => Set KeyCombo
426− -> m (Event t KeyCombo)
427−keyCombos ks = do
428− i <- input
429− return $ fforMaybe i $ \case
430− V.EvKey k m -> if Set.member (k, m) ks
431− then Just (k, m)
432− else Nothing
433− _ -> Nothing
434−
435−-- | A plain split of the available space into vertically stacked panes.
436−-- No visual separator is built in here.
437−splitV :: (Reflex t, Monad m, MonadNodeId m, HasVtyWidgetCtx t m, HasDisplaySize t m)
438− => Dynamic t (Int -> Int)
439− -- ^ Function used to determine size of first pane based on available size
440− -> Dynamic t (Bool, Bool)
441− -- ^ How to focus the two sub-panes, given that we are focused.
442− -> m a
443− -- ^ Widget for first pane
444− -> m b
445− -- ^ Widget for second pane
446− -> m (a,b)
447−splitV sizeFunD focD wA wB = do
448− dw <- displayWidth
449− dh <- displayHeight
450− let regA = Region 0 0 <$> dw <*> (sizeFunD <*> dh)
451− regB = Region 0 <$> (_region_height <$> regA) <*> dw <*> liftA2 (-) dh (_region_height <$> regA)
452− ra <- pane regA (fst <$> focD) wA
453− rb <- pane regB (snd <$> focD) wB
454− return (ra,rb)
455−
456−-- | A plain split of the available space into horizontally stacked panes.
457−-- No visual separator is built in here.
458−splitH :: (Reflex t, Monad m, MonadNodeId m, HasDisplaySize t m, HasVtyWidgetCtx t m)
459− => Dynamic t (Int -> Int)
460− -- ^ Function used to determine size of first pane based on available size
461− -> Dynamic t (Bool, Bool)
462− -- ^ How to focus the two sub-panes, given that we are focused.
463− -> m a
464− -- ^ Widget for first pane
465− -> m b
466− -- ^ Widget for second pane
467− -> m (a,b)
468−splitH sizeFunD focD wA wB = do
469− dw <- displayWidth
470− dh <- displayHeight
471− let regA = Region 0 0 <$> (sizeFunD <*> dw) <*> dh
472− regB = Region <$> (_region_width <$> regA) <*> 0 <*> liftA2 (-) dw (_region_width <$> regA) <*> dh
473− liftA2 (,) (pane regA (fmap fst focD) wA) (pane regB (fmap snd focD) wB)
474−
475−-- | A split of the available space into two parts with a draggable separator.
476−-- Starts with half the space allocated to each, and the first pane has focus.
477−-- Clicking in a pane switches focus.
478−splitVDrag :: (Reflex t, MonadFix m, MonadHold t m, MonadNodeId m, HasDisplaySize t m, HasVtyInput t m, HasVtyWidgetCtx t m)
479− => m ()
480− -> m a
481− -> m b
482− -> m (a,b)
483−splitVDrag wS wA wB = do
484− dh <- displayHeight
485− dw <- displayWidth
486− h0 <- sample $ current dh -- TODO
487− dragE <- drag V.BLeft
488− let splitter0 = h0 `div` 2
489− rec splitterCheckpoint <- holdDyn splitter0 $ leftmost [fst <$> ffilter snd dragSplitter, resizeSplitter]
490− splitterPos <- holdDyn splitter0 $ leftmost [fst <$> dragSplitter, resizeSplitter]
491− splitterFrac <- holdDyn ((1::Double) / 2) $ ffor (attach (current dh) (fst <$> dragSplitter)) $ \(h, x) ->
492− fromIntegral x / max 1 (fromIntegral h)
493− let dragSplitter = fforMaybe (attach (current splitterCheckpoint) dragE) $
494− \(splitterY, Drag (_, fromY) (_, toY) _ _ end) ->
495− if splitterY == fromY then Just (toY, end) else Nothing
496− regA = Region 0 0 <$> dw <*> splitterPos
497− regS = Region 0 <$> splitterPos <*> dw <*> 1
498− regB = Region 0 <$> (splitterPos + 1) <*> dw <*> (dh - splitterPos - 1)
499− resizeSplitter = ffor (attach (current splitterFrac) (updated dh)) $
500− \(frac, h) -> round (frac * fromIntegral h)
501− focA <- holdDyn True $ leftmost
502− [ True <$ mA
503− , False <$ mB
504− ]
505− (mA, rA) <- pane regA focA $ withMouseDown wA
506− pane regS (pure False) wS
507− (mB, rB) <- pane regB (not <$> focA) $ withMouseDown wB
508− return (rA, rB)
509− where
510− withMouseDown x = do
511− m <- mouseDown V.BLeft
512− x' <- x
513− return (m, x')
514−
515−-- | Fill the background with a particular character.
516−fill :: (HasDisplaySize t m, ImageWriter t m) => Char -> m ()
517−fill c = do
518− dw <- displayWidth
519− dh <- displayHeight
520− let fillImg = current $ liftA2 (\w h -> [V.charFill V.defAttr c w h]) dw dh
521− tellImages fillImg
522−
523−-- | Fill the background with the bottom
524−hRule :: (HasDisplaySize t m, ImageWriter t m) => BoxStyle -> m ()
525−hRule boxStyle = fill (_boxStyle_s boxStyle)
526−
527−-- | Defines a set of symbols to use to draw the outlines of boxes
528−-- C.f. https://en.wikipedia.org/wiki/Box-drawing_character
529−data BoxStyle = BoxStyle
530− { _boxStyle_nw :: Char
531− , _boxStyle_n :: Char
532− , _boxStyle_ne :: Char
533− , _boxStyle_e :: Char
534− , _boxStyle_se :: Char
535− , _boxStyle_s :: Char
536− , _boxStyle_sw :: Char
537− , _boxStyle_w :: Char
538− }
552+pane dr foc child = localRegion (const dr) $
553+ mapImages (imagesInRegion $ current dr) $
554+ localFocus (const foc) $
555+ inputInFocusedRegion >>= \e -> localInput (const e) child
539556
540−instance Default BoxStyle where
541− def = singleBoxStyle
542−
543−-- | A box style that uses hyphens and pipe characters. Doesn't handle
544−-- corners very well.
545−hyphenBoxStyle :: BoxStyle
546−hyphenBoxStyle = BoxStyle '-' '-' '-' '|' '-' '-' '-' '|'
547−
548−-- | A single line box style
549−singleBoxStyle :: BoxStyle
550−singleBoxStyle = BoxStyle '┌' '─' '┐' '│' '┘' '─' '└' '│'
551−
552−-- | A thick single line box style
553−thickBoxStyle :: BoxStyle
554−thickBoxStyle = BoxStyle '┏' '━' '┓' '┃' '┛' '━' '┗' '┃'
555−
556−-- | A double line box style
557−doubleBoxStyle :: BoxStyle
558−doubleBoxStyle = BoxStyle '╔' '═' '╗' '║' '╝' '═' '╚' '║'
559−
560−-- | A single line box style with rounded corners
561−roundedBoxStyle :: BoxStyle
562−roundedBoxStyle = BoxStyle '╭' '─' '╮' '│' '╯' '─' '╰' '│'
563−
564−-- | Draws a titled box in the provided style and a child widget inside of that box
565−boxTitle :: (Monad m, Reflex t, MonadNodeId m, HasDisplaySize t m, ImageWriter t m, HasVtyWidgetCtx t m)
566− => Behavior t BoxStyle
567− -> Text
568− -> m a
569− -> m a
570−boxTitle boxStyle title child = do
571− dh <- displayHeight
572− dw <- displayWidth
573− let boxReg = Region 0 0 <$> dw <*> dh
574− innerReg = Region 1 1 <$> (subtract 2 <$> dw) <*> (subtract 2 <$> dh)
575− tellImages (boxImages <$> boxStyle <*> current boxReg)
576− tellImages (fmap (\r -> [regionBlankImage r]) (current innerReg))
577− pane innerReg (pure True) child
578− where
579− boxImages :: BoxStyle -> Region -> [Image]
580− boxImages style (Region left top width height) =
581− let right = left + width - 1
582− bottom = top + height - 1
583− sides =
584− [ withinImage (Region (left + 1) top (width - 2) 1) $
585− V.text' V.defAttr $
586− hPadText title (_boxStyle_n style) (width - 2)
587− , withinImage (Region right (top + 1) 1 (height - 2)) $
588− V.charFill V.defAttr (_boxStyle_e style) 1 (height - 2)
589− , withinImage (Region (left + 1) bottom (width - 2) 1) $
590− V.charFill V.defAttr (_boxStyle_s style) (width - 2) 1
591− , withinImage (Region left (top + 1) 1 (height - 2)) $
592− V.charFill V.defAttr (_boxStyle_w style) 1 (height - 2)
593− ]
594− corners =
595− [ withinImage (Region left top 1 1) $
596− V.char V.defAttr (_boxStyle_nw style)
597− , withinImage (Region right top 1 1) $
598− V.char V.defAttr (_boxStyle_ne style)
599− , withinImage (Region right bottom 1 1) $
600− V.char V.defAttr (_boxStyle_se style)
601− , withinImage (Region left bottom 1 1) $
602− V.char V.defAttr (_boxStyle_sw style)
603− ]
604− in sides ++ if width > 1 && height > 1 then corners else []
605− hPadText :: T.Text -> Char -> Int -> T.Text
606− hPadText t c l = if lt >= l
607− then t
608− else left <> t <> right
609− where
610− lt = T.length t
611− delta = l - lt
612− mkHalf n = T.replicate (n `div` 2) (T.singleton c)
613− left = mkHalf $ delta + 1
614− right = mkHalf delta
615−
616−-- | A box without a title
617−box :: (Monad m, Reflex t, MonadNodeId m, HasDisplaySize t m, ImageWriter t m, HasVtyWidgetCtx t m)
618− => Behavior t BoxStyle
619− -> m a
620− -> m a
621−box boxStyle = boxTitle boxStyle mempty
622−
623−-- | A box whose style is static
624−boxStatic
625− :: (Monad m, Reflex t, MonadNodeId m, HasDisplaySize t m, ImageWriter t m, HasVtyWidgetCtx t m)
626− => BoxStyle
627− -> m a
628− -> m a
629−boxStatic = box . pure
630−
631−-- | Configuration options for displaying "rich" text
632−data RichTextConfig t = RichTextConfig
633− { _richTextConfig_attributes :: Behavior t V.Attr
634− }
635−
636−instance Reflex t => Default (RichTextConfig t) where
637− def = RichTextConfig $ pure V.defAttr
638−
639−-- | A widget that displays text with custom time-varying attributes
640−richText
641− :: (Reflex t, Monad m, HasDisplaySize t m, ImageWriter t m)
642− => RichTextConfig t
643− -> Behavior t Text
644− -> m ()
645−richText cfg t = do
646− dw <- displayWidth
647− let img = (\w a s -> [wrapText w a s])
648− <$> current dw
649− <*> _richTextConfig_attributes cfg
650− <*> t
651− tellImages img
652− where
653− wrapText maxWidth attrs = V.vertCat
654− . concatMap (fmap (V.string attrs . T.unpack) . TZ.wrapWithOffset maxWidth 0)
655− . T.split (=='\n')
656−
657−-- | Renders text, wrapped to the container width
658−text
659− :: (Reflex t, Monad m, HasDisplaySize t m, ImageWriter t m)
660− => Behavior t Text
661− -> m ()
662−text = richText def
663−
664−-- | Scrollable text widget. The output pair exposes the current scroll position and total number of lines (including those
665−-- that are hidden)
666−scrollableText
667− :: forall t m. (Reflex t, MonadHold t m, MonadFix m, HasDisplaySize t m, HasVtyInput t m, ImageWriter t m)
668− => Event t Int
669− -- ^ Number of lines to scroll by
670− -> Behavior t Text
671− -> m (Behavior t (Int, Int))
672− -- ^ (Current scroll position, total number of lines)
673−scrollableText scrollBy t = do
674− dw <- displayWidth
675− let imgs = wrap <$> current dw <*> t
676− kup <- key V.KUp
677− kdown <- key V.KDown
678− m <- mouseScroll
679− let requestedScroll :: Event t Int
680− requestedScroll = leftmost
681− [ 1 <$ kdown
682− , (-1) <$ kup
683− , ffor m $ \case
684− ScrollDirection_Up -> (-1)
685− ScrollDirection_Down -> 1
686− , scrollBy
687− ]
688− updateLine maxN delta ix = min (max 0 (ix + delta)) maxN
689− lineIndex :: Dynamic t Int <- foldDyn (\(maxN, delta) ix -> updateLine (maxN - 1) delta ix) 0 $
690− attach (length <$> imgs) requestedScroll
691− tellImages $ fmap ((:[]) . V.vertCat) $ drop <$> current lineIndex <*> imgs
692− return $ (,) <$> ((+) <$> current lineIndex <*> pure 1) <*> (length <$> imgs)
693− where
694− wrap maxWidth = concatMap (fmap (V.string V.defAttr . T.unpack) . TZ.wrapWithOffset maxWidth 0) . T.split (=='\n')
695−
696−-- | Renders any behavior whose value can be converted to
697−-- 'String' as text
698−display
699− :: (Reflex t, Monad m, Show a, HasDisplaySize t m, ImageWriter t m)
700− => Behavior t a
701− -> m ()
702−display a = text $ T.pack . show <$> a
557+-- * Misc
703558
704559-- | A widget that draws nothing
705560blank :: Monad m => m ()
src/Reflex/Vty/Widget/Box.hsnew file+124
@@ -0,0 +1,124 @@
1+{-|
2+ Description: Drawing boxes in various styles
3+-}
4+module Reflex.Vty.Widget.Box where
5+
6+import Data.Default
7+import Data.Text (Text)
8+import qualified Data.Text as T
9+import Graphics.Vty (Image)
10+import qualified Graphics.Vty as V
11+import Reflex
12+import Reflex.Vty.Widget
13+import Reflex.Vty.Widget.Text
14+
15+-- | Fill the background with the bottom box style
16+hRule :: (HasDisplayRegion t m, HasImageWriter t m, HasTheme t m) => BoxStyle -> m ()
17+hRule boxStyle = fill $ pure (_boxStyle_s boxStyle)
18+
19+-- | Defines a set of symbols to use to draw the outlines of boxes
20+-- C.f. https://en.wikipedia.org/wiki/Box-drawing_character
21+data BoxStyle = BoxStyle
22+ { _boxStyle_nw :: Char
23+ , _boxStyle_n :: Char
24+ , _boxStyle_ne :: Char
25+ , _boxStyle_e :: Char
26+ , _boxStyle_se :: Char
27+ , _boxStyle_s :: Char
28+ , _boxStyle_sw :: Char
29+ , _boxStyle_w :: Char
30+ }
31+
32+instance Default BoxStyle where
33+ def = singleBoxStyle
34+
35+-- | A box style that uses hyphens and pipe characters. Doesn't handle
36+-- corners very well.
37+hyphenBoxStyle :: BoxStyle
38+hyphenBoxStyle = BoxStyle '-' '-' '-' '|' '-' '-' '-' '|'
39+
40+-- | A single line box style
41+singleBoxStyle :: BoxStyle
42+singleBoxStyle = BoxStyle '┌' '─' '┐' '│' '┘' '─' '└' '│'
43+
44+-- | A thick single line box style
45+thickBoxStyle :: BoxStyle
46+thickBoxStyle = BoxStyle '┏' '━' '┓' '┃' '┛' '━' '┗' '┃'
47+
48+-- | A double line box style
49+doubleBoxStyle :: BoxStyle
50+doubleBoxStyle = BoxStyle '╔' '═' '╗' '║' '╝' '═' '╚' '║'
51+
52+-- | A single line box style with rounded corners
53+roundedBoxStyle :: BoxStyle
54+roundedBoxStyle = BoxStyle '╭' '─' '╮' '│' '╯' '─' '╰' '│'
55+
56+-- | Draws a titled box in the provided style and a child widget inside of that box
57+boxTitle :: (Monad m, Reflex t ,HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m, HasTheme t m)
58+ => Behavior t BoxStyle
59+ -> Behavior t Text
60+ -> m a
61+ -> m a
62+boxTitle boxStyle title child = do
63+ dh <- displayHeight
64+ dw <- displayWidth
65+ bt <- theme
66+ let boxReg = Region 0 0 <$> dw <*> dh
67+ innerReg = Region 1 1 <$> (subtract 2 <$> dw) <*> (subtract 2 <$> dh)
68+
69+ tellImages (boxImages <$> bt <*> title <*> boxStyle <*> current boxReg)
70+ tellImages (ffor2 (current innerReg) bt (\r attr -> [regionBlankImage attr r]))
71+
72+ pane innerReg (pure True) child
73+ where
74+ boxImages :: V.Attr -> Text -> BoxStyle -> Region -> [Image]
75+ boxImages attr title' style (Region left top width height) =
76+ let right = left + width - 1
77+ bottom = top + height - 1
78+ sides =
79+ [ withinImage (Region (left + 1) top (width - 2) 1) $
80+ V.text' attr $
81+ hPadText title' (_boxStyle_n style) (width - 2)
82+ , withinImage (Region right (top + 1) 1 (height - 2)) $
83+ V.charFill attr (_boxStyle_e style) 1 (height - 2)
84+ , withinImage (Region (left + 1) bottom (width - 2) 1) $
85+ V.charFill attr (_boxStyle_s style) (width - 2) 1
86+ , withinImage (Region left (top + 1) 1 (height - 2)) $
87+ V.charFill attr (_boxStyle_w style) 1 (height - 2)
88+ ]
89+ corners =
90+ [ withinImage (Region left top 1 1) $
91+ V.char attr (_boxStyle_nw style)
92+ , withinImage (Region right top 1 1) $
93+ V.char attr (_boxStyle_ne style)
94+ , withinImage (Region right bottom 1 1) $
95+ V.char attr (_boxStyle_se style)
96+ , withinImage (Region left bottom 1 1) $
97+ V.char attr (_boxStyle_sw style)
98+ ]
99+ in sides ++ if width > 1 && height > 1 then corners else []
100+ hPadText :: T.Text -> Char -> Int -> T.Text
101+ hPadText t c l = if lt >= l
102+ then t
103+ else left <> t <> right
104+ where
105+ lt = T.length t
106+ delta = l - lt
107+ mkHalf n = T.replicate (n `div` 2) (T.singleton c)
108+ left = mkHalf $ delta + 1
109+ right = mkHalf delta
110+
111+-- | A box without a title
112+box :: (Monad m, Reflex t, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m, HasTheme t m)
113+ => Behavior t BoxStyle
114+ -> m a
115+ -> m a
116+box boxStyle = boxTitle boxStyle mempty
117+
118+-- | A box whose style is static
119+boxStatic
120+ :: (Monad m, Reflex t, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasFocusReader t m, HasTheme t m)
121+ => BoxStyle
122+ -> m a
123+ -> m a
124+boxStatic = box . pure
src/Reflex/Vty/Widget/Input.hs+31−11
@@ -8,16 +8,20 @@ module Reflex.Vty.Widget.Input
88 ) where
99
1010
11+import Reflex.Vty.Widget.Input.Mouse as Export
1112import Reflex.Vty.Widget.Input.Text as Export
1213
1314import Control.Monad (join)
1415import Control.Monad.Fix (MonadFix)
15−import Control.Monad.NodeId (MonadNodeId)
1616import Data.Default (Default(..))
1717import Data.Text (Text)
1818import qualified Graphics.Vty as V
1919import Reflex
2020import Reflex.Vty.Widget
21+import Reflex.Vty.Widget.Box
22+import Reflex.Vty.Widget.Text
23+
24+-- * Buttons
2125
2226-- | Configuration options for the 'button' widget
2327data ButtonConfig t = ButtonConfig
@@ -30,7 +34,7 @@ instance Reflex t => Default (ButtonConfig t) where
3034
3135-- | A button widget that contains a sub-widget
3236button
33− :: (Reflex t, Monad m, MonadNodeId m, HasFocus t m, HasDisplaySize t m, ImageWriter t m, HasVtyWidgetCtx t m, HasVtyInput t m)
37+ :: (Reflex t, Monad m, HasFocusReader t m, HasTheme t m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m)
3438 => ButtonConfig t
3539 -> m ()
3640 -> m (Event t ())
@@ -48,7 +52,7 @@ button cfg child = do
4852
4953-- | A button widget that displays text that can change
5054textButton
51− :: (Reflex t, Monad m, MonadNodeId m, HasDisplaySize t m, HasFocus t m, ImageWriter t m, HasVtyWidgetCtx t m, HasVtyInput t m)
55+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasFocusReader t m, HasTheme t m, HasImageWriter t m, HasInput t m)
5256 => ButtonConfig t
5357 -> Behavior t Text
5458 -> m (Event t ())
@@ -56,31 +60,36 @@ textButton cfg = button cfg . text -- TODO Centering etc.
5660
5761-- | A button widget that displays a static bit of text
5862textButtonStatic
59− :: (Reflex t, Monad m, MonadNodeId m, HasDisplaySize t m, HasFocus t m, ImageWriter t m, HasVtyWidgetCtx t m, HasVtyInput t m)
63+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasFocusReader t m, HasTheme t m, HasImageWriter t m, HasInput t m)
6064 => ButtonConfig t
6165 -> Text
6266 -> m (Event t ())
6367textButtonStatic cfg = textButton cfg . pure
6468
69+-- * Links
70+
6571-- | A clickable link widget
6672link
67− :: (Reflex t, Monad m, HasDisplaySize t m, ImageWriter t m, HasVtyInput t m)
73+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasTheme t m)
6874 => Behavior t Text
6975 -> m (Event t MouseUp)
7076link t = do
77+ bt <- theme
7178 let cfg = RichTextConfig
72− { _richTextConfig_attributes = pure $ V.withStyle V.defAttr V.underline
79+ { _richTextConfig_attributes = fmap (\attr -> V.withStyle attr V.underline) bt
7380 }
7481 richText cfg t
7582 mouseUp
7683
7784-- | A clickable link widget with a static label
7885linkStatic
79− :: (Reflex t, Monad m, ImageWriter t m, HasDisplaySize t m, HasVtyInput t m)
86+ :: (Reflex t, Monad m, HasImageWriter t m, HasDisplayRegion t m, HasInput t m, HasTheme t m)
8087 => Text
8188 -> m (Event t MouseUp)
8289linkStatic = link . pure
8390
91+-- * Checkboxes
92+
8493-- | Characters used to render checked and unchecked textboxes
8594data CheckboxStyle = CheckboxStyle
8695 { _checkboxStyle_unchecked :: Text
@@ -107,30 +116,41 @@ checkboxStyleTick = CheckboxStyle
107116-- | Configuration options for a checkbox
108117data CheckboxConfig t = CheckboxConfig
109118 { _checkboxConfig_checkboxStyle :: Behavior t CheckboxStyle
119+ -- TODO DELETE and use HasTheme instead
110120 , _checkboxConfig_attributes :: Behavior t V.Attr
121+ , _checkboxConfig_setValue :: Event t Bool
111122 }
112123
113124instance (Reflex t) => Default (CheckboxConfig t) where
114125 def = CheckboxConfig
115126 { _checkboxConfig_checkboxStyle = pure def
116127 , _checkboxConfig_attributes = pure V.defAttr
128+ , _checkboxConfig_setValue = never
117129 }
118130
119131-- | A checkbox widget
120132checkbox
121− :: (MonadHold t m, MonadFix m, Reflex t, HasVtyInput t m, HasDisplaySize t m, ImageWriter t m)
133+ :: (MonadHold t m, MonadFix m, Reflex t, HasInput t m, HasDisplayRegion t m, HasImageWriter t m, HasFocusReader t m, HasTheme t m)
122134 => CheckboxConfig t
123135 -> Bool
124136 -> m (Dynamic t Bool)
125137checkbox cfg v0 = do
126138 md <- mouseDown V.BLeft
127139 mu <- mouseUp
128− 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
129148 depressed <- hold mempty $ leftmost
130− [ V.withStyle mempty V.bold <$ md
149+ [ bold <$ md
131150 , mempty <$ mu
132151 ]
133− let attrs = (<>) <$> _checkboxConfig_attributes cfg <*> depressed
152+ let focused = ffor (current f) $ \x -> if x then bold else mempty
153+ let attrs = mconcat <$> sequence [_checkboxConfig_attributes cfg, depressed, focused]
134154 richText (RichTextConfig attrs) $ join . current $ ffor v $ \checked ->
135155 if checked
136156 then _checkboxStyle_checked <$> _checkboxConfig_checkboxStyle cfg
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+18−13
@@ -9,7 +9,6 @@ module Reflex.Vty.Widget.Input.Text
99
1010import Control.Monad (join)
1111import Control.Monad.Fix (MonadFix)
12−import Control.Monad.NodeId (MonadNodeId)
1312import Data.Default (Default(..))
1413import Data.Text (Text)
1514import Data.Text.Zipper
@@ -18,6 +17,7 @@ import Reflex
1817
1918import Reflex.Vty.Widget
2019import Reflex.Vty.Widget.Layout
20+import Reflex.Vty.Widget.Input.Mouse
2121
2222-- | Configuration options for a 'textInput'. For more information on
2323-- 'TextZipper', see 'Data.Text.Zipper'.
@@ -43,7 +43,7 @@ data TextInput t = TextInput
4343
4444-- | A widget that allows text input
4545textInput
46− :: (Reflex t, MonadHold t m, MonadFix m, HasVtyInput t m, HasFocus t m, HasDisplaySize t m, ImageWriter t m)
46+ :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m, HasFocusReader t m, HasTheme t m, HasDisplayRegion t m, HasImageWriter t m, HasDisplayRegion t m)
4747 => TextInputConfig t
4848 -> m (TextInput t)
4949textInput cfg = do
@@ -51,6 +51,8 @@ textInput cfg = do
5151 f <- focus
5252 dh <- displayHeight
5353 dw <- displayWidth
54+ bt <- theme
55+ attr0 <- sample bt
5456 rec v <- foldDyn ($) (_textInputConfig_initialValue cfg) $ mergeWith (.)
5557 [ uncurry (updateTextZipper (_textInputConfig_tabWidth cfg)) <$> attach (current dh) i
5658 , _textInputConfig_modify cfg
@@ -59,13 +61,20 @@ textInput cfg = do
5961 goToDisplayLinePosition mx (st + my) dl
6062 ]
6163 click <- mouseDown V.BLeft
62− let cursorAttrs = ffor f $ \x -> if x then cursorAttributes else V.defAttr
63− let rows = (\w s c -> displayLines w V.defAttr c s)
64+
65+ -- TODO reverseVideo is prob not what we want. Does not work with `darkTheme` in example.hs (cursor is dark rather than light bg)
66+ let toCursorAttrs attr = V.withStyle attr V.reverseVideo
67+ rowInputDyn = (,,)
6468 <$> dw
6569 <*> (mapZipper <$> _textInputConfig_display cfg <*> v)
66− <*> cursorAttrs
70+ <*> f
71+ toDisplayLines attr (w, s, x) =
72+ let c = if x then toCursorAttrs attr else attr
73+ in displayLines w attr c s
74+ attrDyn <- holdDyn attr0 $ pushAlways (\_ -> sample bt) (updated rowInputDyn)
75+ let rows = ffor2 attrDyn rowInputDyn toDisplayLines
6776 img = images . _displayLines_spans <$> rows
68− y <- holdUniqDyn $ _displayLines_cursorY <$> rows
77+ y <- holdUniqDyn $ fmap snd _displayLines_cursorPos <$> rows
6978 let newScrollTop :: Int -> (Int, Int) -> Int
7079 newScrollTop st (h, cursorY)
7180 | cursorY < st = cursorY
@@ -81,7 +90,7 @@ textInput cfg = do
8190
8291-- | A widget that allows multiline text input
8392multilineTextInput
84− :: (Reflex t, MonadHold t m, MonadFix m, HasVtyInput t m, HasFocus t m, HasDisplaySize t m, ImageWriter t m)
93+ :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m, HasFocusReader t m, HasTheme t m, HasDisplayRegion t m, HasImageWriter t m)
8594 => TextInputConfig t
8695 -> m (TextInput t)
8796multilineTextInput cfg = do
@@ -99,22 +108,18 @@ multilineTextInput cfg = do
99108-- the computed line count to greedily size the tile when vertically
100109-- oriented, and uses the fallback width when horizontally oriented.
101110textInputTile
102− :: (Monad m, MonadNodeId m, Reflex t, MonadFix m, MonadLayout t m, HasVtyWidgetCtx t m, HasVtyInput t m, MonadFocus t m)
111+ :: (Monad m, Reflex t, MonadFix m, HasLayout t m, HasInput t m, HasFocus t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m, HasTheme t m)
103112 => m (TextInput t)
104113 -> Dynamic t Int
105114 -> m (TextInput t)
106115textInputTile txt width = do
107116 o <- askOrientation
108− rec (_, t) <- tile (Constraint_Fixed <$> sz) txt
117+ rec t <- tile (Constraint_Fixed <$> sz) txt
109118 let sz = join $ ffor o $ \case
110119 Orientation_Column -> _textInput_lines t
111120 Orientation_Row -> width
112121 return t
113122
114−-- | Default attributes for the text cursor
115−cursorAttributes :: V.Attr
116−cursorAttributes = V.withStyle V.defAttr V.reverseVideo
117−
118123-- | Turn a set of display line rows into a list of images (one per line)
119124images :: [[Span V.Attr]] -> [V.Image]
120125images = map (V.horizCat . map spanToImage)
src/Reflex/Vty/Widget/Layout.hs+315−263
@@ -7,11 +7,13 @@ Description: Monad transformer and tools for arranging widgets and building scre
77module Reflex.Vty.Widget.Layout where
88
99import Control.Applicative (liftA2)
10+import Control.Monad.Morph
1011import Control.Monad.NodeId (MonadNodeId(..), NodeId)
1112import Control.Monad.Reader
1213import Data.List (mapAccumL)
1314import Data.Map.Ordered (OMap)
1415import qualified Data.Map.Ordered as OMap
16+import Data.Maybe (fromMaybe, isNothing)
1517import Data.Ratio ((%))
1618import Data.Semigroup (First(..))
1719import Data.Set.Ordered (OSet)
@@ -21,8 +23,22 @@ import qualified Graphics.Vty as V
2123import Reflex
2224import Reflex.Host.Class (MonadReflexCreateTrigger)
2325import Reflex.Vty.Widget
26+import Reflex.Vty.Widget.Input.Mouse
2427
2528-- * 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
2642
2743-- | Identifies an element that is focusable. Can be created using 'makeFocus'.
2844newtype FocusId = FocusId NodeId
@@ -42,6 +58,13 @@ instance Monoid FocusSet where
4258singletonFS :: FocusId -> FocusSet
4359singletonFS = FocusSet . OSet.singleton
4460
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+
4568-- | Given a 'FocusSet', a currently focused element, and a number of positions
4669-- to move by, determine the newly focused element.
4770shiftFS :: FocusSet -> Maybe FocusId -> Int -> Maybe FocusId
@@ -50,10 +73,20 @@ shiftFS (FocusSet s) fid n = case OSet.findIndex <$> fid <*> pure s of
5073 Just Nothing -> OSet.elemAt s 0
5174 Just (Just ix) -> OSet.elemAt s $ mod (ix + n) (OSet.size s)
5275
53−-- | Operations that change the currently focused element.
54−data Refocus = Refocus_Shift Int -- ^ Shift the focus by a certain number of positions (see 'shiftFS')
55− | Refocus_Id FocusId -- ^ Focus a particular element
56− | Refocus_Clear -- ^ Remove focus from all elements
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))
5790
5891-- | A monad transformer that keeps track of the set of focusable elements and
5992-- which, if any, are currently focused, and allows focus requests.
@@ -73,42 +106,36 @@ newtype Focus t m a = Focus
73106 , PerformEvent t
74107 , NotReady t
75108 , MonadReflexCreateTrigger t
76− , HasDisplaySize t
109+ , HasDisplayRegion t
77110 , PostBuild t
78111 , MonadNodeId
112+ , MonadIO
79113 )
80114
115+
81116instance MonadTrans (Focus t) where
82117 lift = Focus . lift . lift . lift
83118
84−instance (HasVtyInput t m, Monad m) => HasVtyInput t (Focus t m)
119+instance MFunctor (Focus t) where
120+ hoist f = Focus . hoist (hoist (hoist f)) . unFocus
85121
86−instance (HasVtyWidgetCtx t m, Reflex t, MonadFix m) => HasVtyWidgetCtx t (Focus t m) where
87− localCtx f g (Focus w) = Focus $ do
88− d <- ask
89− ((a, fs), e) <- lift $ lift $ lift $ localCtx f g $ runEventWriterT $ flip runReaderT d $ runDynamicWriterT w
90− tellEvent e
91− tellDyn fs
92− return a
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
93127
94−instance ImageWriter t m => ImageWriter t (Focus t m)
128+instance (Reflex t, MonadFix m, HasInput t m) => HasInput t (Focus t m) where
129+ localInput f = hoist (localInput f)
95130
96−instance (HasFocus t m, Monad m) => HasFocus t (Focus t m)
131+instance (HasImageWriter t m, MonadFix m) => HasImageWriter t (Focus t m) where
132+ mapImages f = hoist (mapImages f)
97133
98−-- | A class for things that can produce focusable elements.
99−class (Monad m, Reflex t) => MonadFocus t m | m -> t where
100− -- | Create a focusable element.
101− makeFocus :: m FocusId
102− -- | Emit an 'Event' of requests to change the focus.
103− requestFocus :: Event t Refocus -> m ()
104− -- | Produce a 'Dynamic' that indicates whether the given 'FocusId' is focused.
105− isFocused :: FocusId -> m (Dynamic t Bool)
106− -- | Run an action, additionally returning the focusable elements it produced.
107− subFoci :: m a -> m (a, Dynamic t FocusSet)
108− -- | Get a 'Dynamic' of the currently focused element identifier.
109− focusedId :: m (Dynamic t (Maybe FocusId))
134+instance (HasFocusReader t m, Monad m) => HasFocusReader t (Focus t m)
110135
111−instance (Reflex t, MonadFix m, MonadNodeId m) => MonadFocus t (Focus t m) where
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
112139 makeFocus = do
113140 fid <- FocusId <$> lift getNextNodeId
114141 Focus $ tellDyn $ pure $ singletonFS fid
@@ -137,53 +164,99 @@ runFocus (Focus x) = do
137164 f (fs, rf) mf = case getFirst rf of
138165 Refocus_Clear -> Nothing
139166 Refocus_Id fid -> Just fid
140− Refocus_Shift n -> shiftFS fs mf n
141−
142−-- | A widget that is focusable and occupies a layout region based on the
143−-- provided constraint. Returns the 'FocusId' allowing for manual focus
144−-- management.
145−tile'
146− :: (MonadNodeId m, MonadFix m, Reflex t, HasVtyWidgetCtx t m, HasVtyInput t m, MonadFocus t m, MonadLayout t m)
147− => Dynamic t Constraint
167+ Refocus_Shift n -> if n < 0 && isNothing mf
168+ then shiftFS fs (OSet.elemAt (unFocusSet fs) 0) n
169+ else shiftFS fs mf n
170+
171+-- | Runs an action in the focus monad, providing it with information about
172+-- whether any of the foci created within it are focused.
173+anyChildFocused
174+ :: (HasFocus t m, MonadFix m)
175+ => (Dynamic t Bool -> m a)
148176 -> m a
149− -> m (FocusId, a)
150−tile' c w = do
151− fid <- makeFocus
152− r <- region c
153− fid' <- focusedId
154− parentFocused <- isFocused fid
155− rec (click, result, subs) <- pane r focused $ do
156− m <- mouseDown V.BLeft
157− (x, sf) <- subFoci w
158− pure (m, x, sf)
159− let childFocused = liftA2 (\f s -> case f of
177+anyChildFocused f = do
178+ fid <- focusedId
179+ rec (a, fs) <- subFoci (f b)
180+ let b = liftA2 (\foc s -> case foc of
160181 Nothing -> False
161− Just f' -> OSet.member f' $ unFocusSet s) fid' subs
162− let focused = (||) <$> parentFocused <*> childFocused
163− requestFocus $ Refocus_Id fid <$ click
164− pure (fid, result)
182+ Just f' -> OSet.member f' $ unFocusSet s) fid fs
183+ pure a
165184
166−-- | A widget that is focusable and occupies a layout region based on the
167−-- provided constraint.
168−tile
169− :: (MonadNodeId m, MonadFix m, Reflex t, HasVtyWidgetCtx t m, HasVtyInput t m, MonadFocus t m, MonadLayout t m)
170− => Dynamic t Constraint
171− -> m a
172− -> m a
173−tile c = fmap snd . tile' c
185+-- ** Focus controls
174186
175−-- | A widget that is not focusable and occupies a layout region based on the
176−-- provided constraint.
177−unfocusable
178− :: (Reflex t, MonadNodeId m, HasVtyWidgetCtx t m, MonadLayout t m)
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]
194+
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
212+
213+-- | Datatype representing constraints on a widget's size along the main axis (see 'Orientation')
214+data Constraint = Constraint_Fixed Int
215+ | Constraint_Min Int
216+ deriving (Show, Read, Eq, Ord)
217+
218+-- | Shorthand for constructing a fixed constraint
219+fixed
220+ :: Reflex t
221+ => Dynamic t Int
222+ -> Dynamic t Constraint
223+fixed = fmap Constraint_Fixed
224+
225+-- | Shorthand for constructing a minimum size constraint
226+stretch
227+ :: Reflex t
228+ => Dynamic t Int
229+ -> Dynamic t Constraint
230+stretch = fmap Constraint_Min
231+
232+-- | Shorthand for constructing a constraint of no minimum size
233+flex
234+ :: Reflex t
179235 => Dynamic t Constraint
236+flex = pure $ Constraint_Min 0
237+
238+-- *** Orientation
239+
240+-- | The main-axis orientation of a 'Layout' widget
241+data Orientation = Orientation_Column
242+ | Orientation_Row
243+ deriving (Show, Read, Eq, Ord)
244+
245+-- | Create a row-oriented 'axis'
246+row
247+ :: (Reflex t, MonadFix m, HasLayout t m)
248+ => m a
180249 -> m a
250+row = axis (pure Orientation_Row) flex
251+
252+-- | Create a column-oriented 'axis'
253+col
254+ :: (Reflex t, MonadFix m, HasLayout t m)
255+ => m a
181256 -> m a
182−unfocusable c w = do
183− r <- region c
184− pane r (pure False) w
257+col = axis (pure Orientation_Column) flex
185258
186−-- * Layout
259+-- ** Layout management data
187260
188261-- | A collection of information related to the layout of the screen. The root
189262-- node is a "parent" widget, and the contents of the 'LayoutForest' are its
@@ -222,158 +295,6 @@ rootLT (LayoutTree a _) = a
222295childrenLT :: LayoutTree a -> LayoutForest a
223296childrenLT (LayoutTree _ a) = a
224297
225−-- | A monad transformer that collects layout constraints and provides a layout
226−-- solution that satisfies those constraints.
227−newtype Layout t m a = Layout
228− { unLayout :: DynamicWriterT t (LayoutForest (Constraint, Orientation))
229− (ReaderT (Dynamic t (LayoutTree (Region, Orientation))) m) a
230− }
231− deriving
232− ( Functor
233− , Applicative
234− , Monad
235− , MonadHold t
236− , MonadSample t
237− , MonadFix
238− , TriggerEvent t
239− , PerformEvent t
240− , NotReady t
241− , MonadReflexCreateTrigger t
242− , HasDisplaySize t
243− , PostBuild t
244− , MonadNodeId
245− )
246−
247−instance MonadTrans (Layout t) where
248− lift = Layout . lift . lift
249−
250−instance (HasVtyWidgetCtx t m, HasDisplaySize t m, Reflex t, MonadFix m) => HasVtyWidgetCtx t (Layout t m) where
251− localCtx f g x = do
252− solution <- Layout ask
253− let orientation = snd . rootLT <$> solution
254− lift $ localCtx f g $ do
255− dw <- displayWidth
256− dh <- displayHeight
257− let reg = Region 0 0 <$> dw <*> dh
258− runLayout orientation reg x
259−
260−instance (HasVtyInput t m, Monad m) => HasVtyInput t (Layout t m)
261−
262−instance ImageWriter t m => ImageWriter t (Layout t m)
263−
264−instance (HasFocus t m, Monad m) => HasFocus t (Layout t m)
265−
266−-- | A class of operations for creating screen layouts.
267−class Monad m => MonadLayout t m | m -> t where
268− -- | Creates a parent element in the current layout with the given size
269− -- constraint, which lays out its children according to the provided
270− -- orientation.
271− axis :: Dynamic t Orientation -> Dynamic t Constraint -> m a -> m a
272− -- | Creates a child element in the current layout with the given size
273− -- constraint, returning the 'Region' that the child element is allocated.
274− region :: Dynamic t Constraint -> m (Dynamic t Region)
275− -- | Returns the orientation of the containing 'axis'.
276− askOrientation :: m (Dynamic t Orientation)
277−
278−instance (Monad m, MonadNodeId m, Reflex t, MonadFix m) => MonadLayout t (Layout t m) where
279− axis o c (Layout x) = Layout $ do
280− nodeId <- getNextNodeId
281− (result, forest) <- lift $ local (\t -> (\(Just a) -> a) . lookupLF nodeId . childrenLT <$> t) $ runDynamicWriterT x
282− tellDyn $ singletonLF nodeId <$> (LayoutTree <$> ((,) <$> c <*> o) <*> forest)
283− pure result
284− region c = do
285− nodeId <- lift getNextNodeId
286− Layout $ tellDyn $ ffor c $ \c' -> singletonLF nodeId $ LayoutTree (c', Orientation_Row) mempty
287− solutions <- Layout ask
288− pure $ maybe (error "region: could not find layout solution") (fst . rootLT) . lookupLF nodeId . childrenLT <$> solutions
289− askOrientation = Layout $ asks $ fmap (snd . rootLT)
290−
291−instance (MonadFix m, MonadFocus t m) => MonadFocus t (Layout t m) where
292− makeFocus = lift makeFocus
293− requestFocus = lift . requestFocus
294− isFocused = lift . isFocused
295− focusedId = lift focusedId
296− subFoci (Layout x) = Layout $ do
297− y <- ask
298− ((a, w), sf) <- lift $ lift $ subFoci $ flip runReaderT y $ runDynamicWriterT x
299− tellDyn w
300− pure (a, sf)
301−
302−-- | Datatype representing constraints on a widget's size along the main axis (see 'Orientation')
303−data Constraint = Constraint_Fixed Int
304− | Constraint_Min Int
305− deriving (Show, Read, Eq, Ord)
306−
307−-- | The main-axis orientation of a 'Layout' widget
308−data Orientation = Orientation_Column
309− | Orientation_Row
310− deriving (Show, Read, Eq, Ord)
311−
312−-- | Create a row-oriented 'axis'
313−row
314− :: (Reflex t, MonadNodeId m, MonadFix m, MonadLayout t m)
315− => m a
316− -> m a
317−row = axis (pure Orientation_Row) (pure $ Constraint_Min 0)
318−
319−-- | Create a column-oriented 'axis'
320−col
321− :: (Reflex t, MonadNodeId m, MonadFix m, MonadLayout t m)
322− => m a
323− -> m a
324−col = axis (pure Orientation_Column) (pure $ Constraint_Min 0)
325−
326−-- | A tile constrained to the given size. Returns the 'FocusId' of the tile,
327−-- allowing for manual focus management.
328−fixed'
329− :: (Reflex t, MonadNodeId m, MonadFix m, HasVtyWidgetCtx t m, HasVtyInput t m, MonadFocus t m, MonadLayout t m)
330− => Dynamic t Int
331− -> m a
332− -> m (FocusId, a)
333−fixed' n = tile' (Constraint_Fixed <$> n)
334−
335−-- | A tile constrained to the given size.
336−fixed
337− :: (Reflex t, MonadNodeId m, MonadFix m, HasVtyWidgetCtx t m, HasVtyInput t m, MonadFocus t m, MonadLayout t m)
338− => Dynamic t Int
339− -> m a
340− -> m a
341−fixed n = fmap snd . fixed' n
342−
343−-- | A tile that can grow, but will always occupy at least the given minimum
344−-- size. Returns the 'FocusId' of the tile, allowing for manual focus
345−-- management.
346−stretch'
347− :: (Reflex t, MonadNodeId m, MonadFix m, HasVtyWidgetCtx t m, HasVtyInput t m, MonadFocus t m, MonadLayout t m)
348− => Dynamic t Int
349− -> m a
350− -> m (FocusId, a)
351−stretch' minSz = tile' (Constraint_Min <$> minSz)
352−
353−-- | A tile that can grow, but will always occupy at least the given minimum
354−-- size.
355−stretch
356− :: (Reflex t, MonadNodeId m, MonadFix m, HasVtyWidgetCtx t m, HasVtyInput t m, MonadFocus t m, MonadLayout t m)
357− => Dynamic t Int
358− -> m a
359− -> m a
360−stretch minSz = fmap snd . stretch' minSz
361−
362−-- | A tile that can grow or shrink without constraint. Returns the 'FocusId'
363−-- of the tile, allowing for manual focus management.
364−flex'
365− :: (Reflex t, MonadNodeId m, MonadFix m, HasVtyWidgetCtx t m, HasVtyInput t m, MonadFocus t m, MonadLayout t m)
366− => m a
367− -> m (FocusId, a)
368−flex' = tile' (pure $ Constraint_Min 0)
369−
370−-- | A tile that can grow or shrink without constraint.
371−flex
372− :: (Reflex t,MonadNodeId m, MonadFix m, HasVtyWidgetCtx t m, HasVtyInput t m, MonadFocus t m, MonadLayout t m)
373− => m a
374− -> m a
375−flex = fmap snd . flex'
376−
377298-- | Produce a layout solution given a starting orientation, the overall screen
378299-- size, and a set of constraints.
379300solve
@@ -401,16 +322,27 @@ solve o0 r0 (LayoutForest cs) =
401322 -> [(a, Constraint)]
402323 -> [(a, Int)]
403324 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
404327 let minTotal = sum $ ffor constraints $ \case
405328 (_, Constraint_Fixed n) -> n
406329 (_, Constraint_Min n) -> n
330+ -- The leftover space is the area we can allow stretchable items to
331+ -- expand into
407332 leftover = max 0 (available - minTotal)
333+ -- The number of stretchable items that will try to share some of the
334+ -- leftover space
408335 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)
409339 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.
410342 adjustment = max 0 $ available - minTotal - szStretch * numStretch
411343 in snd $ mapAccumL (\adj (a, c) -> case c of
412344 Constraint_Fixed n -> (adj, (a, n))
413− Constraint_Min n -> (0, (a, n + szStretch + adj))) adjustment constraints
345+ Constraint_Min n -> (max 0 (adj-1), (a, n + szStretch + signum adj))) adjustment constraints
414346 isMin (Constraint_Min _) = True
415347 isMin _ = False
416348
@@ -427,6 +359,106 @@ chunk o r (offset, sz) = case o of
427359 , _region_width = sz
428360 }
429361
362+-- ** The layout monad
363+
364+-- | A class of operations for creating screen layouts.
365+class Monad m => HasLayout t m | m -> t where
366+ -- | Starts a parent element in the current layout with the given size
367+ -- constraint, which lays out its children according to the provided
368+ -- orientation.
369+ axis :: Dynamic t Orientation -> Dynamic t Constraint -> m a -> m a
370+ -- | Creates a child element in the current layout with the given size
371+ -- constraint, returning the 'Region' that the child element is allocated.
372+ region :: Dynamic t Constraint -> m (Dynamic t Region)
373+ -- | Returns the orientation of the containing 'axis'.
374+ askOrientation :: m (Dynamic t Orientation)
375+
376+-- | A monad transformer that collects layout constraints and provides a layout
377+-- solution that satisfies those constraints.
378+newtype Layout t m a = Layout
379+ { unLayout :: DynamicWriterT t (LayoutForest (Constraint, Orientation))
380+ (ReaderT (Dynamic t (LayoutTree (Region, Orientation))) m) a
381+ }
382+ deriving
383+ ( Functor
384+ , Applicative
385+ , HasDisplayRegion t
386+ , Monad
387+ , MonadFix
388+ , MonadHold t
389+ , MonadIO
390+ , MonadNodeId
391+ , MonadReflexCreateTrigger t
392+ , MonadSample t
393+ , NotReady t
394+ , PerformEvent t
395+ , PostBuild t
396+ , TriggerEvent t
397+ )
398+
399+instance MonadTrans (Layout t) where
400+ lift = Layout . lift . lift
401+
402+instance MFunctor (Layout t) where
403+ hoist f = Layout . hoist (hoist f) . unLayout
404+
405+instance (Adjustable t m, MonadFix m, MonadHold t m) => Adjustable t (Layout t m) where
406+ runWithReplace (Layout a) e = Layout $ runWithReplace a $ fmap unLayout e
407+ traverseIntMapWithKeyWithAdjust f m e = Layout $ traverseIntMapWithKeyWithAdjust (\k v -> unLayout $ f k v) m e
408+ traverseDMapWithKeyWithAdjust f m e = Layout $ traverseDMapWithKeyWithAdjust (\k v -> unLayout $ f k v) m e
409+ traverseDMapWithKeyWithAdjustWithMove f m e = Layout $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unLayout $ f k v) m e
410+
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+
430462-- | Runs a 'Layout' action, using the given orientation and region to
431463-- calculate layout solutions.
432464runLayout
@@ -440,58 +472,78 @@ runLayout o r (Layout x) = do
440472 let solutions = solve <$> o <*> r <*> w
441473 return result
442474
443−-- | Request focus be shifted backward and forward based on tab presses. <Tab>
444−-- shifts focus forward and <Shift+Tab> shifts focus backward.
445−tabNavigation :: (Reflex t, MonadNodeId m, HasVtyInput t m, MonadFocus t m) => m ()
446−tabNavigation = do
447− fwd <- fmap (const 1) <$> key (V.KChar '\t')
448− back <- fmap (const (-1)) <$> key V.KBackTab
449− requestFocus $ Refocus_Shift <$> leftmost [fwd, back]
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
478+ dw <- displayWidth
479+ dh <- displayHeight
480+ let r = Region 0 0 <$> dw <*> dh
481+ runLayout (pure Orientation_Column) r f
450482
451−test :: IO ()
452−test = mainWidget $ do
453− inp <- input
454− let box' w = do
455− f <- focus
456− box (ffor (current f) $ \x -> if x then doubleBoxStyle else singleBoxStyle) $ w
457− text' t = do
458− f <- focus
459− box' $ richText (RichTextConfig $ current $ (\x -> if x then V.withStyle V.defAttr V.underline else V.defAttr) <$> f) t
460− initManager_ $ do
461− tabNavigation
462− col $ do
463− fixed 5 $ box' $ row $ do
464− flex $ do
465− text' "asdf"
466− flex $ text' "xyz"
467− fixed 5 $ do
468− text' " 1"
469− row $ do
470− flex $ do
471− text' "2"
472− flex $ text' "3"
473− flex $ text' "xyz"
474− flex $ text' "xyz"
475− flex $ text' "asdf"
476− flex $ text' "asdf"
477− return $ fforMaybe inp $ \case
478− V.EvKey (V.KChar 'c') [V.MCtrl] -> Just ()
479− _ -> Nothing
483+-- * The tile "window manager"
484+--
485+-- $tiling
486+-- Generally HasLayout and HasFocus are used together to build a user
487+-- interface. These functions check the available screen size and initialize
488+-- the layout monad with that information, and also initialize the focus monad.
480489
481490-- | Initialize a 'Layout' and 'Focus' management context, returning the produced 'FocusSet'.
482491initManager
483− :: (HasDisplaySize t m, Reflex t, MonadHold t m, MonadFix m)
492+ :: (HasDisplayRegion t m, Reflex t, MonadHold t m, MonadFix m)
484493 => Layout t (Focus t m) a
485494 -> m (a, Dynamic t FocusSet)
486−initManager f = do
487− dw <- displayWidth
488− dh <- displayHeight
489− let r = Region 0 0 <$> dw <*> dh
490− runFocus $ runLayout (pure Orientation_Column) r f
495+initManager =
496+ runFocus . initLayout
491497
492498-- | Initialize a 'Layout' and 'Focus' management context.
493499initManager_
494− :: (HasDisplaySize t m, Reflex t, MonadHold t m, MonadFix m)
500+ :: (HasDisplayRegion t m, Reflex t, MonadHold t m, MonadFix m)
495501 => Layout t (Focus t m) a
496502 -> m a
497503initManager_ = fmap fst . initManager
504+
505+-- ** Layout tiles
506+
507+-- *** Focusable
508+
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
537+
538+-- *** Unfocusable
539+
540+-- | A widget that is not focusable and occupies a layout region based on the
541+-- provided constraint.
542+grout
543+ :: (Reflex t, HasLayout t m, HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
544+ => Dynamic t Constraint
545+ -> m a
546+ -> m a
547+grout c w = do
548+ r <- region c
549+ pane r (pure True) w
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