on develop help sign in
.github/workflows/haskell.ymlnew file+41
since splitsnapshots
unifiedsplit
@@ -0,0 +1,41 @@
1+name: github-action
2+
3+on: [push, pull_request]
4+
5+jobs:
6+ build:
7+ strategy:
8+ matrix:
9+ ghc: ['8.4.4', '8.6.5', '8.8.4', '8.10.2']
10+ os: ['ubuntu-latest', 'macos-latest']
11+ runs-on: ${{ matrix.os }}
12+
13+ name: GHC ${{ matrix.ghc }} on ${{ matrix.os }}
14+ steps:
15+ - uses: actions/checkout@v2
16+ - uses: actions/setup-haskell@v1
17+ with:
18+ ghc-version: ${{ matrix.ghc }}
19+ - name: Cache
20+ uses: actions/cache@v1
21+ env:
22+ cache-name: cache-cabal
23+ with:
24+ path: ~/.cabal
25+ key: ${{ runner.os }}-${{ matrix.ghc }}-build-${{ env.cache-name }}-${{ hashFiles('**/*.cabal') }}-${{ hashFiles('**/cabal.project') }}
26+ restore-keys: |
27+ ${{ runner.os }}-${{ matrix.ghc }}-build-${{ env.cache-name }}-
28+ ${{ runner.os }}-${{ matrix.ghc }}-build-
29+ ${{ runner.os }}-${{ matrix.ghc }}-
30+ ${{ runner.os }}
31+
32+ - name: Install dependencies
33+ run: |
34+ cabal update
35+ cabal build --only-dependencies --enable-tests --enable-benchmarks
36+ - name: Build
37+ run: cabal build --enable-tests --enable-benchmarks all
38+ - name: Run tests
39+ run: cabal test all
40+ - name: Build Docs
41+ run: cabal haddock
.gitignore+1
@@ -1,2 +1,3 @@
1tags
1tags
2dist/
2dist/
3+dist-newstyle/
CONTRIBUTING.mdnew file+68
@@ -0,0 +1,68 @@
1+# Contribution Guide
2+
3+Contributions and issue reports are encouraged and appreciated!
4+
5+- [Opening Issues](#opening-issues)
6+- [Submitting Changes](#submitting-changes)
7+ - [Guidelines for Commit Messages](#guidelines-for-commit-messages)
8+ - [Guidelines for Pull Requests](#guidelines-for-pull-requests)
9+ - [Code Quality](#code-quality)
10+ - [Documentation](#documentation)
11+
12+## Opening Issues
13+
14+Before opening an issue, please check whether your issue has already been reported. Assuming it has not:
15+
16+* Describe the issue you're encountering or the suggestion you're making
17+* Include any relevant steps to reproduce or code samples you can. It's always easier for us to debug if we have something that demonstrates the error.
18+* Let us know what version of reflex-vty you were using. If you're using a github checkout, provide the git hash.
19+* Describe how you're building reflex-vty (i.e., via reflex-platform, cabal install, stack, obelisk, etc.).
20+
21+## Submitting Changes
22+
23+### Guidelines for Commit Messages
24+
25+#### Summary Line
26+The summary line of your commit message should summarize the changes being made. Commit messages should be written in the imperative mood and should describe what happens when the commit is applied.
27+
28+One way to think about it is that your commit message should be able to complete the sentence:
29+"When applied, this commit will..."
30+
31+#### Body
32+For breaking changes, new features, refactors, or other major changes, the body of the commit message should describe the motivation behind the change in greater detail and may include references to the issue tracker. The body shouldn't repeat code/comments from the diff.
33+
34+### Guidelines for Pull Requests
35+
36+Wherever possible, pull requests should add a single feature or fix a single bug. Pull requests should not bundle several unrelated changes.
37+
38+### Code Quality
39+
40+#### Warnings
41+
42+Your pull request should add no new warnings to the project. It should also generally not disable any warnings.
43+
44+#### Build and Test
45+
46+Make sure the project builds and that the tests pass! This will generally also be checked by CI before merge, but trying it yourself first means you'll catch problems earlier and your contribution can be merged that much sooner!
47+
48+To check that things are still working, run:
49+
50+```bash
51+nix-build release.nix
52+```
53+
54+### Documentation
55+
56+#### In the code
57+We're always striving to improve documentation. Please include [haddock](https://haskell-haddock.readthedocs.io/en/latest/index.html) documentation for any added code, and update the documentation for any code you modify.
58+
59+#### In the [Changelog](ChangeLog.md)
60+Add an entry to the changelog when your PR:
61+* Adds a feature
62+* Deprecates something
63+* Includes a breaking change
64+* Makes any other change that will impact users
65+
66+#### In the [Readme](README.md)
67+The readme is the first place a lot of people look for information about the repository. Update any parts of the readme that are affected by your PR.
68+
ChangeLog.mdnew file+128
@@ -0,0 +1,128 @@
1+# Revision history for reflex-vty
2+
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
94+
95+## 0.1.4.1
96+* Migrate to new dependent-sum / dependent-map (after the "some" package split)
97+
98+## 0.1.4.0
99+* ([#15](https://github.com/reflex-frp/reflex-vty/pull/15)) Add `PostBuild` instance for `Layout`.
100+* ([#17](https://github.com/reflex-frp/reflex-vty/pull/17)) Add `splitH` to implement horizontal functionality of `splitV`.
101+* ([#19](https://github.com/reflex-frp/reflex-vty/pull/19)) Add `boxTitle`: a box with a title.
102+* ([#19](https://github.com/reflex-frp/reflex-vty/pull/19)) Update the text editing example to use `boxTitle`.
103+* ([#21](https://github.com/reflex-frp/reflex-vty/pull/21)) Fix bug in `drag` that caused dragging with different mouse button to trigger the click event.
104+* ([#22](https://github.com/reflex-frp/reflex-vty/pull/22)) Add support for GHC 8.8.
105+
106+## 0.1.3.0
107+* Add `mouseScroll` to capture scroll wheel events.
108+* Add `scrollableText`: a text display widget that can be scrolled using the mouse or keyboard.
109+* Add widget to the example executable that displays scrollable text.
110+
111+## 0.1.2.1
112+* Add `keyCombo` function (single-key-combination version of `keyCombos`).
113+* Use upstream `NotReady` instances instead of orphans defined in this package if reflex-0.6.3 is available.
114+
115+## 0.1.2.0
116+* Allow `TextZipper` contents to be transformed before being displayed.
117+* Fix bug in `row` orientation.
118+* Handle wrapping of lines containing full-width unicode characters in `textInput`.
119+
120+## 0.1.1.1
121+* Bump minimum version of reflex.
122+
123+## 0.1.1.0
124+* Set version bounds in cabal file.
125+* Add travis CI config.
126+
127+## 0.1.0.0
128+* Initial release
README.mdnew file+42
@@ -0,0 +1,42 @@
1+# reflex-vty
2+
3+[![Haskell](https://img.shields.io/badge/language-Haskell-orange.svg)](https://haskell.org) [![Hackage](https://img.shields.io/hackage/v/reflex-vty.svg)](https://hackage.haskell.org/package/reflex-vty) [![Hackage CI](https://matrix.hackage.haskell.org/api/v2/packages/reflex-vty/badge)](https://matrix.hackage.haskell.org/#/package/reflex-vty) [![Travis CI](https://api.travis-ci.org/reflex-frp/reflex-vty.svg?branch=develop)](https://travis-ci.org/reflex-frp/reflex-vty) [![BSD3 License](https://img.shields.io/badge/license-BSD3-blue.svg)](https://github.com/reflex-frp/reflex-vty/blob/master/LICENSE)
4+
5+Build terminal applications using functional reactive programming (FRP) with [Reflex FRP](https://reflex-frp.org).
6+
7+![Example Animation](https://i.imgur.com/FULQNtu.gif)
8+
9+Feature requests, pull requests, and other feedback are welcome and appreciated (see the [contribution guide](CONTRIBUTING.md)). This library
10+is still experimental, so big changes are possible!
11+### How to Build
12+
13+#### With reflex-platform
14+
15+Enter a nix-shell for the project:
16+```bash
17+git clone https://github.com/reflex-frp/reflex-vty.git
18+cd reflex-vty
19+nix-shell
20+```
21+
22+From within the nix-shell you can:
23+* Run the example: `cabal repl example`
24+* Load the library in the repl: `cabal repl reflex-vty`
25+* Build the example executable: `cabal build example`
26+* Build the docs: `cabal haddock`
27+* Run ghcid for immediate compiler feedback when you save a .hs file: `ghcid -c "cabal repl reflex-vty --ghc-options=-Wall"`
28+* etc.
29+
30+#### With cabal
31+
32+Please see the `tested-with` field of the cabal file for known-compatible versions of GHC.
33+
34+From the reflex-vty project directory:
35+
36+```bash
37+# nix-shell -p cabal-install binutils icu # for nix users
38+cabal new-configure
39+cabal new-build # to build the library and example
40+cabal new-repl # to enter a repl for the library
41+cabal new-repl example # to enter a repl for the example executable
42+```
doc/qa.mdnew file+41
@@ -0,0 +1,41 @@
1+# QA Guide
2+
3+This QA guide takes you through the process of testing `reflex-vty` from a visual observer's perspective. The goal is to ensure that `reflex-vty` does not contain bugs/regressions that are visual in nature, which are hard to detect with automated testing. This guide assumes you have Nix installed.
4+
5+## Steps
6+
7+Clone and examples
8+
9+```bash
10+nix-shell -p git
11+cd $(mktemp -d)
12+git clone https://github.com/reflex-frp/reflex-vty.git --branch develop
13+nix run -f reflex-vty/release.nix reflex-vty --command example
14+```
15+
16+The build may take a while to complete.
17+
18+Once it's done you can test the following:
19+
20+ * Make sure that resizing your terminal window causes the editor to resize as well.
21+
22+ * Click on `Todo List`.
23+ * Make sure you can check tasks, uncheck tasks, add new tasks, and edit the text of tasks.
24+ * Make sure that resizing your terminal window causes the editor to resize as well.
25+ * Type <kbd>Esc</kbd>.
26+
27+ * Click on `Text Editor`.
28+ * Make sure you can edit text in the "Text Edit" box. Try entering lots of text so that it doesn't all fit. It should scroll down as you type. Using they arrow keys on your keyboard should move the cursor and let you go back up and see text that was out of view before.
29+ * Make sure that clicking, right-clicking, using the scroll-wheel on your mouse, and dragging in the "Text Edit" box updates the message in the box lying above and behind the "Text Edit" box.
30+ * Make sure that dragging your mouse cursor around inside the lowest box causes the message to update. This should update only when dragging *inside* that box, not outside of it.
31+ * Make sure that resizing your terminal window causes the boxes to resize as well.
32+ * Type <kbd>Esc</kbd>.
33+
34+ * Click on `Scrollable text display`.
35+ * Make sure that you can scroll up and down through the small box of text with your mouse's scroll wheel.
36+ * Make sure that resizing your terminal window causes the boxes to resize as well.
37+ * Type <kbd>Esc</kbd>.
38+
39+ * Type <kbd>Ctrl</kbd>+<kbd>C</kbd> to quit.
40+
41+There should be only 3 examples to cover. If there are more or less than this then this QA guide is out of date and QA should fail.
doc/tasks.pngnew file

Binary file not shown.

reflex-platform/default.nixnew file+2
@@ -0,0 +1,2 @@
1+# DO NOT HAND-EDIT THIS FILE
2+import (import ./thunk.nix)
reflex-platform/github.jsonnew file+8
@@ -0,0 +1,8 @@
1+{
2+ "owner": "reflex-frp",
3+ "repo": "reflex-platform",
4+ "branch": "develop",
5+ "private": false,
6+ "rev": "f019863c21ee85498e6a6e0072e617b2462b70ed",
7+ "sha256": "146xfjqdwd55s9jg1ggi6akcxxxd5c0pvc4bpjx3whwiikpcv8y4"
8+}
reflex-platform/thunk.nixnew file+9
@@ -0,0 +1,9 @@
1+# DO NOT HAND-EDIT THIS FILE
2+let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:
3+ if !fetchSubmodules && !private then builtins.fetchTarball {
4+ url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256;
5+ } else (import <nixpkgs> {}).fetchFromGitHub {
6+ inherit owner repo rev sha256 fetchSubmodules private;
7+ };
8+ json = builtins.fromJSON (builtins.readFile ./github.json);
9+in fetch json
reflex-vty.cabal+121โˆ’22
@@ -1,48 +1,99 @@
1name: reflex-vty
1name: reflex-vty
2โˆ’version: 0.1.0.0
2+version: 0.2.0.0
3โˆ’synopsis: Reflex FRP host and widgets for vty applications
3+synopsis: Reflex FRP host and widgets for VTY applications
4โˆ’description: Host and widget library for Reflex-based FRP applications
4+description:
5+ Build terminal applications using functional reactive programming (FRP) with Reflex FRP (<https://reflex-frp.org>).
6+ .
7+ <<https://i.imgur.com/FULQNtu.gif>>
5license: BSD3
8license: BSD3
6license-file: LICENSE
9license-file: LICENSE
7author: Obsidian Systems LLC
10author: Obsidian Systems LLC
8maintainer: maintainer@obsidian.systems
11maintainer: maintainer@obsidian.systems
12+copyright: 2020 Obsidian Systems LLC
9category: FRP
13category: FRP
10build-type: Simple
14build-type: Simple
11โˆ’cabal-version: >=1.10
15+cabal-version: 1.18
16+extra-source-files:
17+ README.md
18+ ChangeLog.md
19+extra-doc-files: doc/tasks.png
20+tested-with:
21+ GHC ==8.4.4 || ==8.6.5 || ==8.8.4 || ==8.10.2
22+
23+source-repository head
24+ type: git
25+ location: https://github.com/reflex-frp/reflex-vty
12
26
13library
27library
14 exposed-modules: Reflex.Vty
28 exposed-modules: Reflex.Vty
15 , Reflex.Vty.Host
29 , Reflex.Vty.Host
16 , Reflex.Vty.Widget
30 , Reflex.Vty.Widget
31+ , Reflex.Vty.Widget.Box
17 , Reflex.Vty.Widget.Input
32 , Reflex.Vty.Widget.Input
33+ , Reflex.Vty.Widget.Input.Mouse
18 , Reflex.Vty.Widget.Input.Text
34 , Reflex.Vty.Widget.Input.Text
35+ , Reflex.Vty.Widget.Layout
36+ , Reflex.Vty.Widget.Split
37+ , Reflex.Vty.Widget.Text
19 , Data.Text.Zipper
38 , Data.Text.Zipper
20โˆ’ , Control.Monad.Writer.Adjustable
21โˆ’ , Reflex.Class.Switchable
22โˆ’ , Reflex.NotReady.Class.Orphans
23 , Reflex.Spider.Orphans
39 , Reflex.Spider.Orphans
40+ , Control.Monad.NodeId
24 build-depends:
41 build-depends:
25โˆ’ base,
42+ base >= 4.10.0 && < 4.15,
43+ bimap >= 0.3.3 && < 0.5,
26โˆ’ containers,
44+ containers >= 0.5.0 && < 0.7,
45+ mtl >= 2.2.2 && < 2.3,
46+ transformers >= 0.5.5 && < 0.6,
47+ stm >= 2.4 && < 2.6,
27โˆ’ data-default,
48+ data-default >= 0.7.1 && < 0.8,
28โˆ’ dependent-map,
49+ dependent-map >= 0.4 && < 0.5,
50+ text >= 1.2.3 && < 1.3,
29โˆ’ dependent-sum,
51+ dependent-sum >= 0.7 && < 0.8,
30โˆ’ mtl,
52+ exception-transformers >= 0.4.0 && < 0.5,
53+ mmorph >= 1.1 && < 1.2,
54+ ordered-containers >= 0.2.2 && < 0.3,
31โˆ’ primitive,
55+ primitive >= 0.6.3 && < 0.8,
32โˆ’ ref-tf,
56+ ref-tf >= 0.4.0 && < 0.5,
33โˆ’ reflex,
57+ reflex >= 0.8 && < 0.9,
34โˆ’ stm,
35โˆ’ text,
36โˆ’ time,
58+ time >= 1.8.0 && < 1.10,
37โˆ’ transformers,
38โˆ’ vty
59+ vty >= 5.28 && < 5.29
39 hs-source-dirs: src
60 hs-source-dirs: src
40 default-language: Haskell2010
61 default-language: Haskell2010
62+ ghc-options: -Wall
63+ default-extensions:
64+ BangPatterns
65+ ConstraintKinds
66+ CPP
67+ DataKinds
68+ DefaultSignatures
69+ DeriveFunctor
70+ DeriveGeneric
71+ FlexibleContexts
72+ FlexibleInstances
73+ FunctionalDependencies
74+ GADTs
75+ GeneralizedNewtypeDeriving
76+ KindSignatures
77+ LambdaCase
78+ MultiParamTypeClasses
79+ MultiWayIf
80+ OverloadedStrings
81+ PatternGuards
82+ PolyKinds
83+ QuasiQuotes
84+ RankNTypes
85+ RecursiveDo
86+ ScopedTypeVariables
87+ StandaloneDeriving
88+ TemplateHaskell
89+ TupleSections
90+ TypeApplications
91+ TypeFamilies
41
92
42executable example
93executable example
43 hs-source-dirs: src-bin
94 hs-source-dirs: src-bin
44 main-is: example.hs
95 main-is: example.hs
45โˆ’ ghc-options: -threaded
96+ ghc-options: -threaded -Wall
46 build-depends:
97 build-depends:
47 base,
98 base,
48 containers,
99 containers,
@@ -53,3 +104,51 @@ executable example
53 transformers,
104 transformers,
54 vty
105 vty
55 default-language: Haskell2010
106 default-language: Haskell2010
107+ other-modules: Example.CPU
108+ default-extensions:
109+ BangPatterns
110+ ConstraintKinds
111+ CPP
112+ DataKinds
113+ DefaultSignatures
114+ DeriveFunctor
115+ DeriveGeneric
116+ FlexibleContexts
117+ FlexibleInstances
118+ FunctionalDependencies
119+ GADTs
120+ GeneralizedNewtypeDeriving
121+ KindSignatures
122+ LambdaCase
123+ MultiParamTypeClasses
124+ MultiWayIf
125+ OverloadedStrings
126+ PatternGuards
127+ PolyKinds
128+ QuasiQuotes
129+ RankNTypes
130+ RecursiveDo
131+ ScopedTypeVariables
132+ StandaloneDeriving
133+ TemplateHaskell
134+ TupleSections
135+ TypeApplications
136+ TypeFamilies
137+
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
release.nixnew file+16
@@ -0,0 +1,16 @@
1+{ reflex-platform ? import ./reflex-platform {}
2+}:
3+let inherit (reflex-platform.nixpkgs.haskell) lib;
4+ ghc = reflex-platform.ghc.override {
5+ overrides = self: super: {
6+ vty = self.callHackage "vty" "5.28" {};
7+ ansi-terminal = self.callHackage "ansi-terminal" "0.10.3" {};
8+ ansi-wl-pprint = self.callHackage "ansi-wl-pprint" "0.6.9" {};
9+ test-framework = lib.doJailbreak super.test-framework;
10+ };
11+ };
12+in
13+{
14+ reflex-vty =
15+ ghc.callCabal2nix "reflex-vty" ./. {};
16+}
shell.nixnew file+1
@@ -0,0 +1 @@
1+(import ./release.nix {}).reflex-vty.env
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+249โˆ’121
@@ -1,69 +1,150 @@
1โˆ’{-# LANGUAGE FlexibleContexts #-}
1+import Control.Applicative
2โˆ’{-# LANGUAGE GADTs #-}
2+import Control.Monad
3โˆ’{-# LANGUAGE LambdaCase #-}
4โˆ’{-# LANGUAGE OverloadedStrings #-}
5โˆ’{-# LANGUAGE RankNTypes #-}
6โˆ’{-# LANGUAGE RecursiveDo #-}
7โˆ’{-# LANGUAGE ScopedTypeVariables #-}
8โˆ’{-# LANGUAGE TupleSections #-}
9โˆ’{-# OPTIONS_GHC -threaded #-}
10โˆ’
11import Control.Monad.Fix
3import Control.Monad.Fix
12โˆ’import Data.Foldable
4+import Data.Functor
5+import Data.Functor.Misc
13import Data.Map (Map)
6import Data.Map (Map)
14import qualified Data.Map as Map
7import qualified Data.Map as Map
15โˆ’import Data.Sequence (Seq)
16โˆ’import qualified Data.Sequence as Seq
8+import qualified Data.Set as Set
17import Data.Text (Text)
9import Data.Text (Text)
18import qualified Data.Text as T
10import qualified Data.Text as T
19import qualified Data.Text.Zipper as TZ
11import qualified Data.Text.Zipper as TZ
20import qualified Graphics.Vty as V
12import qualified Graphics.Vty as V
21import Reflex
13import Reflex
22import Reflex.Network
14import Reflex.Network
23โˆ’import Reflex.NotReady.Class
24import Reflex.Vty
15import Reflex.Vty
25
16
26โˆ’main :: IO ()
17+import Example.CPU
27โˆ’main = mainWidget $ do
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+
34+data Example = Example_TextEditor
35+ | Example_Todo
36+ | Example_ScrollableTextDisplay
37+ | Example_ClickButtonsGetEmojis
38+ | Example_CPUStat
39+ deriving (Show, Read, Eq, Ord, Enum, Bounded)
40+
41+withCtrlC :: (Monad m, HasInput t m, Reflex t) => m () -> m (Event t ())
42+withCtrlC f = do
28 inp <- input
43 inp <- input
44+ f
29โˆ’ tellShutdown . fforMaybe inp $ \case
45+ return $ fforMaybe inp $ \case
30 V.EvKey (V.KChar 'c') [V.MCtrl] -> Just ()
46 V.EvKey (V.KChar 'c') [V.MCtrl] -> Just ()
31 _ -> Nothing
47 _ -> Nothing
32โˆ’ let btn = button $ pure "Add another task"
33โˆ’ rec let todos'' = todos' [Todo "First" True, Todo "Second" False, Todo "Third" False] $ () <$ e
34โˆ’ (m, (e, _)) <- splitV (pure (subtract 6)) (pure (True, True)) todos'' $ do
35โˆ’ splitV (pure (subtract 3)) (pure (True, True)) btn (display $ current m)
36โˆ’ return ()
37
48
38โˆ’testBoxes :: (Reflex t, MonadHold t m, MonadFix m) => VtyWidget t m ()
49+darkTheme :: V.Attr
39โˆ’testBoxes = do
50+darkTheme = V.Attr {
40โˆ’ size <- displaySize
51+ V.attrStyle = V.SetTo V.standout
41โˆ’ let region1 = fmap (\(w,h) -> Region (w `div` 6) (h `div` 6) (w `div` 2) (h `div` 2)) size
52+ , V.attrForeColor = V.SetTo V.black
42โˆ’ region2 = fmap (\(w,h) -> Region (w `div` 4) (h `div` 4) (2 * (w `div` 3)) (2*(h `div` 3))) size
53+ , V.attrBackColor = V.Default
43โˆ’ pane region1 (constDyn False) . box singleBoxStyle $ debugInput
54+ , V.attrURL = V.Default
44โˆ’ _ <- pane region2 (constDyn True) . box singleBoxStyle $
55+}
45โˆ’ splitVDrag (hRule doubleBoxStyle) (box roundedBoxStyle $ multilineTextInput def) (box roundedBoxStyle dragTest)
46โˆ’ return ()
47
56
48โˆ’debugFocus :: (Reflex t, Monad m) => VtyWidget t m ()
57+main :: IO ()
49โˆ’debugFocus = do
58+main = mainWidget $ withCtrlC $ do
50โˆ’ f <- focus
59+ initManager_ $ do
51โˆ’ text $ T.pack . show <$> current f
60+ tabNavigation
52โˆ’
61+ let gf = grout . fixed
53โˆ’debugInput :: (Reflex t, MonadHold t m) => VtyWidget t m ()
62+ t = tile flex
54โˆ’debugInput = do
63+ buttons = col $ do
55โˆ’ lastEvent <- hold "No event yet" . fmap show =<< input
64+ gf 3 $ col $ do
56โˆ’ text $ T.pack <$> lastEvent
65+ gf 1 $ text "Select an example."
57โˆ’
66+ gf 1 $ text "Esc will bring you back here."
58โˆ’dragTest :: (Reflex t, MonadHold t m, MonadFix m) => VtyWidget t m ()
67+ gf 1 $ text "Ctrl+c to quit."
59โˆ’dragTest = do
68+ a <- t $ textButtonStatic def "Todo List"
60โˆ’ lastEvent <- hold "No event yet" . fmap show =<< drag V.BLeft
69+ b <- t $ textButtonStatic def "Text Editor"
61โˆ’ text $ T.pack <$> lastEvent
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 ()
62
94
63โˆ’testStringBox :: (Reflex t, Monad m) => VtyWidget t m ()
95+-- * Mouse button and emojis example
64โˆ’testStringBox = box singleBoxStyle .
96+easyExample :: (VtyExample t m, Manager t m, MonadHold t m) => m (Event t ())
65โˆ’ text . pure . T.pack . take 500 $ cycle ('\n' : ['a'..'z'])
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."
119+ inp <- input
120+ return $ fforMaybe inp $ \case
121+ V.EvKey (V.KChar 'c') [V.MCtrl] -> Just ()
122+ _ -> Nothing
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]
66
132
133+-- * Task list example
134+taskList
135+ :: (VtyExample t m, Manager t m, MonadHold t m, Adjustable t m, PostBuild t m)
136+ => m ()
137+taskList = col $ do
138+ let todos0 =
139+ [ Todo "Find reflex-vty" True
140+ , Todo "Become functional reactive" False
141+ , Todo "Make vty apps" False
142+ ]
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
147+ return ()
67
148
68data Todo = Todo
149data Todo = Todo
69 { _todo_label :: Text
150 { _todo_label :: Text
@@ -71,85 +152,132 @@ data Todo = Todo
71 }
152 }
72 deriving (Show, Read, Eq, Ord)
153 deriving (Show, Read, Eq, Ord)
73
154
74โˆ’checkbox
155+data TodoOutput t = TodoOutput
75โˆ’ :: (MonadHold t m, MonadFix m, Reflex t)
156+ { _todoOutput_todo :: Dynamic t Todo
76โˆ’ => Bool
157+ , _todoOutput_delete :: Event t ()
77โˆ’ -> VtyWidget t m (Dynamic t Bool)
158+ , _todoOutput_height :: Dynamic t Int
78โˆ’checkbox v0 = do
159+ , _todoOutput_focusId :: FocusId
79โˆ’ i <- input
160+ }
80โˆ’ v <- toggle v0 $ fforMaybe i $ \case
81โˆ’ V.EvMouseUp _ _ _ -> Just ()
82โˆ’ _ -> Nothing
83โˆ’ text $ current $ ffor v $ \v' -> if v' then "[x]" else "[ ]"
84โˆ’ return v
85โˆ’
86โˆ’button :: (Reflex t, Monad m) => Behavior t Text -> VtyWidget t m (Event t ())
87โˆ’button t = do
88โˆ’ box roundedBoxStyle $ text t
89โˆ’ fmap (() <$) mouseUp
90
161
91todo
162todo
92โˆ’ :: (MonadHold t m, MonadFix m, Reflex t)
163+ :: (VtyExample t m, Manager t m, MonadHold t m)
93 => Todo
164 => Todo
94โˆ’ -> VtyWidget t m (Dynamic t Todo)
165+ -> m (TodoOutput t)
95โˆ’todo t0 = do
166+todo t0 = row $ do
96โˆ’ w <- displayWidth
167+ let toggleKeys = Set.fromList
97โˆ’ let checkboxWidth = 3
168+ [ (V.KChar ' ', [V.MCtrl])
98โˆ’ checkboxRegion = pure $ Region 0 0 checkboxWidth 1
169+ , (V.KChar '@', [V.MCtrl])
99โˆ’ labelRegion = ffor w $ \w' -> Region (checkboxWidth + 1) 0 (w' - 1 - checkboxWidth) 1
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+ }
100โˆ’ value <- pane checkboxRegion (pure True) $ checkbox $ _todo_done t0
177+ value <- tile (fixed 4) $ checkbox cfg $ _todo_done t0
101โˆ’ label <- pane labelRegion (pure True) $
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
102โˆ’ textInput $ def { _textInputConfig_initialValue = TZ.fromText $ _todo_label t0 }
182+ { _textInputConfig_initialValue = TZ.fromText $ _todo_label t0 }
103โˆ’ return $ Todo <$> label <*> value
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+ }
191+ where
192+ backspaceOnEmpty v = \case
193+ V.EvKey V.KBS _ | T.null v -> Just ()
194+ _ -> Nothing
104
195
105todos
196todos
106โˆ’ :: (MonadHold t m, MonadFix m, Reflex t, Adjustable t m, NotReady t m, PostBuild t m)
197+ :: forall t m.
198+ ( MonadHold t m
199+ , Manager t m
200+ , VtyExample t m
201+ , Adjustable t m
202+ , PostBuild t m
203+ )
107 => [Todo]
204 => [Todo]
108 -> Event t ()
205 -> Event t ()
109โˆ’ -> VtyWidget t m (Dynamic t (Seq Todo))
206+ -> m (Dynamic t (Map Int (TodoOutput t)))
110todos todos0 newTodo = do
207todos todos0 newTodo = do
111โˆ’ rec todos <- foldDyn ($) (Seq.fromList todos0) $ leftmost
112โˆ’ [ (\ts -> ts Seq.|> Todo "" False) <$ newTodo
113โˆ’ , (\(ix, t) -> Seq.update ix t) <$> updates
114โˆ’ ]
115โˆ’ w <- displayWidth
116โˆ’ listOut <- networkView $ ffor todos $ \ts' ->
117โˆ’ flip Seq.traverseWithIndex ts' $ \row t -> do
118โˆ’ let reg = fmap (\w' -> Region 0 row w' 1) w
119โˆ’ pane reg (fmap (==row) selected) $ do
120โˆ’ e <- mouseUp
121โˆ’ r <- todo t
122โˆ’ return (row <$ e, (row,) <$> updated r)
123โˆ’ selectionClick <- switchHold never $
124โˆ’ fmap (leftmost . toList . fmap fst) listOut
125โˆ’ selected <- holdDyn 0 $ leftmost
126โˆ’ [ selectionClick
127โˆ’ , Seq.length <$> tag (current todos) newTodo
128โˆ’ ]
129โˆ’ updates <- switchHold never $ fmap (leftmost . toList . fmap snd) listOut
130โˆ’ return todos
131โˆ’
132โˆ’todos'
133โˆ’ :: (MonadHold t m, MonadFix m, Reflex t, Adjustable t m, NotReady t m, PostBuild t m)
134โˆ’ => [Todo]
135โˆ’ -> Event t ()
136โˆ’ -> VtyWidget t m (Dynamic t (Map Int Todo))
137โˆ’todos' todos0 newTodo = do
138 let todosMap0 = Map.fromList $ zip [0..] todos0
208 let todosMap0 = Map.fromList $ zip [0..] todos0
139โˆ’ w <- displayWidth
140โˆ’ rec listOut <- listHoldWithKey todosMap0 insert $ \row t -> do
209+ rec listOut <- listHoldWithKey todosMap0 updates $ \k t -> grout (fixed 1) $ do
141โˆ’ let reg = fmap (\w' -> Region 0 row w' 1) w
142โˆ’ pane reg (fmap (==row) selected) $ do
143โˆ’ e <- mouseUp
144โˆ’ r <- todo t
210+ to <- todo t
145โˆ’ return (row <$ e, r)
211+ let sel = select selectOnDelete $ Const2 k
146โˆ’ let selectionClick = switch . current $ fmap (leftmost . Map.elems . fmap fst) listOut
212+ pb <- getPostBuild
147โˆ’ selected <- holdDyn 0 $ leftmost
213+ requestFocus $ Refocus_Id (_todoOutput_focusId to) <$ leftmost [pb, sel]
148โˆ’ [ selectionClick
214+ pure to
149โˆ’ , Map.size <$> tag (current todosMap) newTodo
215+ let delete = flip Map.singleton Nothing <$> todoDelete
150โˆ’ ]
216+ todosMap = joinDynThroughMap $ fmap _todoOutput_todo <$> listOut
151โˆ’ let todosMap = joinDynThroughMap $ fmap (fmap snd) listOut
217+ insert = ffor (tag (current todosMap) newTodo) $ \m -> case Map.lookupMax m of
152โˆ’ let insert = ffor (tagPromptlyDyn todosMap newTodo) $ \m -> case Map.lookupMax m of
218+ Nothing -> Map.singleton 0 $ Just $ Todo "" False
153โˆ’ Nothing -> Map.singleton 0 $ Just $ Todo "" False
219+ Just (k, _) -> Map.singleton (k+1) $ Just $ Todo "" False
154โˆ’ Just (k, _) -> {- Map.union (Just <$> m) $ -} Map.singleton (k+1) $ Just $ Todo "" False
220+ updates = leftmost [insert, delete]
155โˆ’ return todosMap
221+ todoDelete = switch . current $
222+ leftmost . Map.elems . Map.mapWithKey (\k -> (k <$) . _todoOutput_delete) <$> listOut
223+
224+ selectOnDelete = fanMap $ (`Map.singleton` ()) <$> attachWithMaybe
225+ (\m k -> let (before, after) = Map.split k m
226+ in fmap fst $ Map.lookupMax before <|> Map.lookupMin after)
227+ (current todosMap)
228+ todoDelete
229+ return listOut
230+
231+-- * Scrollable text example
232+
233+scrolling :: (VtyExample t m, MonadHold t m, Manager t m, PostBuild t m) => m ()
234+scrolling = col $ do
235+ grout (fixed 2) $ text "Use your mouse wheel or up and down arrows to scroll:"
236+ (fid, out) <- tile' (fixed 5) $ boxStatic def $ scrollableText never $ "Gallia est omnis divisa in partes tres, quarum unam incolunt Belgae, aliam Aquitani, tertiam qui ipsorum lingua Celtae, nostra Galli appellantur. Hi omnes lingua, institutis, legibus inter se differunt. Gallos ab Aquitanis Garumna flumen, a Belgis Matrona et Sequana dividit. Horum omnium fortissimi sunt Belgae, propterea quod a cultu atque humanitate provinciae longissime absunt, minimeque ad eos mercatores saepe commeant atque ea quae ad effeminandos animos pertinent important, proximique sunt Germanis, qui trans Rhenum incolunt, quibuscum continenter bellum gerunt. Qua de causa Helvetii quoque reliquos Gallos virtute praecedunt, quod fere cotidianis proeliis cum Germanis contendunt, cum aut suis finibus eos prohibent aut ipsi in eorum finibus bellum gerunt. Eorum una pars, quam Gallos obtinere dictum est, initium capit a flumine Rhodano, continetur Garumna flumine, Oceano, finibus Belgarum, attingit etiam ab Sequanis et Helvetiis flumen Rhenum, vergit ad septentriones. Belgae ab extremis Galliae finibus oriuntur, pertinent ad inferiorem partem fluminis Rheni, spectant in septentrionem et orientem solem. Aquitania a Garumna flumine ad Pyrenaeos montes et eam partem Oceani quae est ad Hispaniam pertinet; spectat inter occasum solis et septentriones.\nApud Helvetios longe nobilissimus fuit et ditissimus Orgetorix. Is M. Messala, [et P.] M. Pisone consulibus regni cupiditate inductus coniurationem nobilitatis fecit et civitati persuasit ut de finibus suis cum omnibus copiis exirent: perfacile esse, cum virtute omnibus praestarent, totius Galliae imperio potiri. Id hoc facilius iis persuasit, quod undique loci natura Helvetii continentur: una ex parte flumine Rheno latissimo atque altissimo, qui agrum Helvetium a Germanis dividit; altera ex parte monte Iura altissimo, qui est inter Sequanos et Helvetios; tertia lacu Lemanno et flumine Rhodano, qui provinciam nostram ab Helvetiis dividit. His rebus fiebat ut et minus late vagarentur et minus facile finitimis bellum inferre possent; qua ex parte homines bellandi cupidi magno dolore adficiebantur. Pro multitudine autem hominum et pro gloria belli atque fortitudinis angustos se fines habere arbitrabantur, qui in longitudinem milia passuum CCXL, in latitudinem CLXXX patebant."
237+ pb <- getPostBuild
238+ requestFocus $ Refocus_Id fid <$ pb
239+ grout (fixed 1) $ text $ ffor out $ \(ix, total) -> "Scrolled to line " <> T.pack (show ix) <> " of " <> T.pack (show total)
240+
241+-- * Text editor example with resizable boxes
242+
243+testBoxes
244+ :: (MonadHold t m, VtyExample t m)
245+ => m ()
246+testBoxes = do
247+ dw <- displayWidth
248+ dh <- displayHeight
249+ let region1 = Region <$> (div' dw 6) <*> (div' dh 6) <*> (div' dw 2) <*> (div' dh 2)
250+ region2 = Region <$> (div' dw 4) <*> (div' dh 4) <*> (2 * div' dw 3) <*> (2 * div' dh 3)
251+ pane region1 (constDyn False) . boxStatic singleBoxStyle $ debugInput
252+ _ <- pane region2 (constDyn True) . boxStatic singleBoxStyle $
253+ let cfg = def
254+ { _textInputConfig_initialValue =
255+ "This box is a text input. The box below responds to mouse drag inputs. You can also drag the separator between the boxes to resize them."
256+ }
257+ textBox = boxTitle (pure roundedBoxStyle) "Text Edit" $
258+ multilineTextInput cfg
259+ dragBox = boxStatic roundedBoxStyle dragTest
260+ in splitVDrag (hRule doubleBoxStyle) textBox dragBox
261+ return ()
262+ where
263+ div' :: (Integral a, Applicative f) => f a -> f a -> f a
264+ div' = liftA2 div
265+
266+debugFocus :: (VtyExample t m) => m ()
267+debugFocus = do
268+ f <- focus
269+ text $ T.pack . show <$> current f
270+
271+debugInput :: (VtyExample t m, MonadHold t m) => m ()
272+debugInput = do
273+ lastEvent <- hold "No event yet" . fmap show =<< input
274+ text $ T.pack <$> lastEvent
275+
276+dragTest :: (VtyExample t m, MonadHold t m) => m ()
277+dragTest = do
278+ lastEvent <- hold "No event yet" . fmap show =<< drag V.BLeft
279+ text $ T.pack <$> lastEvent
280+
281+testStringBox :: VtyExample t m => m ()
282+testStringBox = boxStatic singleBoxStyle .
283+ text . pure . T.pack . take 500 $ cycle ('\n' : ['a'..'z'])
src/Control/Monad/NodeId.hsnew file+78
@@ -0,0 +1,78 @@
1+{-|
2+Module: Control.Monad.NodeId
3+Description: Monad providing a supply of unique identifiers
4+-}
5+{-# Language UndecidableInstances #-}
6+module Control.Monad.NodeId
7+ ( NodeId
8+ , MonadNodeId (..)
9+ , NodeIdT
10+ , runNodeIdT
11+ ) where
12+
13+import Control.Monad.Morph
14+import Control.Monad.Reader
15+import Control.Monad.Ref
16+import Data.IORef
17+
18+import Reflex
19+import Reflex.Host.Class
20+
21+-- | A unique identifier with respect to the 'runNodeIdT' in which it was generated
22+newtype NodeId = NodeId Integer
23+ deriving (Eq, Ord, Show)
24+
25+-- | Members of this class can request new identifiers that are unique in the action
26+-- in which they are obtained (i.e., all calls to 'getNextNodeId' in a given 'runNodeIdT'
27+-- will produce unique results)
28+class Monad m => MonadNodeId m where
29+ getNextNodeId :: m NodeId
30+ default getNextNodeId :: (MonadTrans t, MonadNodeId n, m ~ t n) => m NodeId
31+ getNextNodeId = lift getNextNodeId
32+
33+-- | A monad transformer that internally keeps track of the next 'NodeId'
34+newtype NodeIdT m a = NodeIdT { unNodeIdT :: ReaderT (IORef NodeId) m a }
35+ deriving
36+ ( Functor
37+ , Applicative
38+ , MFunctor
39+ , Monad
40+ , MonadFix
41+ , MonadHold t
42+ , MonadIO
43+ , MonadRef
44+ , MonadReflexCreateTrigger t
45+ , MonadSample t
46+ , MonadTrans
47+ , NotReady t
48+ , PerformEvent t
49+ , PostBuild t
50+ , TriggerEvent t
51+ )
52+
53+instance MonadNodeId m => MonadNodeId (ReaderT x m)
54+instance MonadNodeId m => MonadNodeId (BehaviorWriterT t x m)
55+instance MonadNodeId m => MonadNodeId (DynamicWriterT t x m)
56+instance MonadNodeId m => MonadNodeId (EventWriterT t x m)
57+instance MonadNodeId m => MonadNodeId (TriggerEventT t m)
58+instance MonadNodeId m => MonadNodeId (PostBuildT t m)
59+
60+instance Adjustable t m => Adjustable t (NodeIdT m) where
61+ runWithReplace (NodeIdT a) e = NodeIdT $ runWithReplace a $ fmap unNodeIdT e
62+ traverseIntMapWithKeyWithAdjust f m e = NodeIdT $ traverseIntMapWithKeyWithAdjust (\k v -> unNodeIdT $ f k v) m e
63+ traverseDMapWithKeyWithAdjust f m e = NodeIdT $ traverseDMapWithKeyWithAdjust (\k v -> unNodeIdT $ f k v) m e
64+ traverseDMapWithKeyWithAdjustWithMove f m e = NodeIdT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unNodeIdT $ f k v) m e
65+
66+-- | Runs a 'NodeIdT' action
67+runNodeIdT :: MonadIO m => NodeIdT m a -> m a
68+runNodeIdT a = do
69+ ref <- liftIO $ newIORef $ NodeId 0
70+ runReaderT (unNodeIdT a) ref
71+
72+instance MonadIO m => MonadNodeId (NodeIdT m) where
73+ getNextNodeId = NodeIdT $ do
74+ ref <- ask
75+ liftIO $ newNodeId ref
76+
77+newNodeId :: IORef NodeId -> IO NodeId
78+newNodeId ref = atomicModifyIORef' ref $ \(NodeId n) -> (NodeId $ succ n, NodeId n)
src/Control/Monad/Writer/Adjustable.hsdeletedโˆ’35
@@ -1,35 +0,0 @@
1โˆ’{-|
2โˆ’Module: Control.Monad.Writer.Adjustable
3โˆ’Description: An Adjustable instance for WriterT
4โˆ’-}
5โˆ’{-# LANGUAGE FlexibleInstances #-}
6โˆ’{-# LANGUAGE MultiParamTypeClasses #-}
7โˆ’{-# LANGUAGE ScopedTypeVariables #-}
8โˆ’{-# LANGUAGE UndecidableInstances #-}
9โˆ’{-# OPTIONS_GHC -fno-warn-orphans #-} -- TODO find a better home for this
10โˆ’module Control.Monad.Writer.Adjustable where
11โˆ’
12โˆ’import Control.Monad.Trans
13โˆ’import Control.Monad.Trans.Writer
14โˆ’import qualified Data.Dependent.Map as DMap
15โˆ’import Data.Foldable (fold)
16โˆ’import Data.Functor.Compose
17โˆ’import Reflex
18โˆ’import Reflex.Patch.DMapWithMove
19โˆ’import Reflex.Class.Switchable
20โˆ’
21โˆ’instance (Adjustable t m, MonadHold t m, Switchable t w, Monoid w) => Adjustable t (WriterT w m) where
22โˆ’ runWithReplace a0 a' = do
23โˆ’ (w, r) <- lift $ runWithReplace (runWriterT a0) $ fmap (runWriterT) a'
24โˆ’ tell =<< switching (snd w) (snd <$> r)
25โˆ’ return $ (fst w, fst <$> r)
26โˆ’ traverseIntMapWithKeyWithAdjust f dm0 dm' = do
27โˆ’ (w, r) <- lift $ traverseIntMapWithKeyWithAdjust (\k v -> runWriterT (f k v)) dm0 dm'
28โˆ’ tell =<< switching (fold $ fmap snd w) (fmap (mconcat . patchIntMapNewElements . fmap snd) r)
29โˆ’ return (fmap fst w, fmap (fmap fst) r)
30โˆ’ traverseDMapWithKeyWithAdjustWithMove f dm0 dm' = do
31โˆ’ (w, r) <- lift $ traverseDMapWithKeyWithAdjustWithMove (\k v -> fmap (\(x, w') -> Compose (w', x)) $ runWriterT (f k v)) dm0 dm'
32โˆ’ let w' = runWriter $ DMap.traverseWithKey (\_ (Compose (w'', x)) -> tell w'' >> return x) w
33โˆ’ r' = fmap (runWriter . traversePatchDMapWithMove (\(Compose (w'', x)) -> tell w'' >> return x)) r
34โˆ’ tell =<< switching (snd w') (fmap snd r')
35โˆ’ return (fst w', fmap fst r')
src/Data/Text/Zipper.hs+328โˆ’83
@@ -7,20 +7,28 @@ Description: A zipper for text documents that allows convenient editing and navi
7'TextZipper's can be converted into 'DisplayLines', which describe how the contents of the zipper will be displayed when wrapped to fit within a container of a certain width. It also provides some convenience facilities for converting interactions with the rendered DisplayLines back into manipulations of the underlying TextZipper.
7'TextZipper's can be converted into 'DisplayLines', which describe how the contents of the zipper will be displayed when wrapped to fit within a container of a certain width. It also provides some convenience facilities for converting interactions with the rendered DisplayLines back into manipulations of the underlying TextZipper.
8
8
9-}
9-}
10โˆ’{-# LANGUAGE FlexibleContexts #-}
11โˆ’{-# LANGUAGE LambdaCase #-}
12โˆ’{-# LANGUAGE OverloadedStrings #-}
13module Data.Text.Zipper where
10module Data.Text.Zipper where
14
11
12+import Prelude
13+
14+import Control.Exception (assert)
15+import Control.Monad.State (evalState, forM, get, put)
15import Data.Char (isSpace)
16import Data.Char (isSpace)
16import Data.Map (Map)
17import Data.Map (Map)
17โˆ’import qualified Data.Map as Map
18import Data.Maybe (fromMaybe)
18import Data.Maybe (fromMaybe)
19โˆ’import Control.Monad.State (evalState, forM, get, put)
19+import Data.String
20โˆ’
21import Data.Text (Text)
20import Data.Text (Text)
21+import Data.Text.Internal (Text(..), text)
22+import Data.Text.Internal.Fusion (stream)
23+import Data.Text.Internal.Fusion.Types (Stream(..), Step(..))
24+import Data.Text.Unsafe
25+import qualified Data.List as L
26+import qualified Data.Map as Map
22import qualified Data.Text as T
27import qualified Data.Text as T
23
28
29+import Graphics.Text.Width (wcwidth)
30+
31+
24-- | A zipper of the logical text input contents (the "document"). The lines
32-- | A zipper of the logical text input contents (the "document"). The lines
25-- before the line containing the cursor are stored in reverse order.
33-- before the line containing the cursor are stored in reverse order.
26-- The cursor is logically between the "before" and "after" text.
34-- The cursor is logically between the "before" and "after" text.
@@ -35,6 +43,18 @@ data TextZipper = TextZipper
35 }
43 }
36 deriving (Show)
44 deriving (Show)
37
45
46+instance IsString TextZipper where
47+ fromString = fromText . T.pack
48+
49+-- | Map a replacement function over the characters in a 'TextZipper'
50+mapZipper :: (Char -> Char) -> TextZipper -> TextZipper
51+mapZipper f (TextZipper lb b a la) = TextZipper
52+ { _textZipper_linesBefore = fmap (T.map f) lb
53+ , _textZipper_before = T.map f b
54+ , _textZipper_after = T.map f a
55+ , _textZipper_linesAfter = fmap (T.map f) la
56+ }
57+
38-- | Move the cursor left one character, if possible
58-- | Move the cursor left one character, if possible
39left :: TextZipper -> TextZipper
59left :: TextZipper -> TextZipper
40left = leftN 1
60left = leftN 1
@@ -81,6 +101,18 @@ down z@(TextZipper lb b a la) = case la of
81 let (b', a') = T.splitAt (T.length b) l
101 let (b', a') = T.splitAt (T.length b) l
82 in TextZipper ((b <> a) : lb) b' a' ls
102 in TextZipper ((b <> a) : lb) b' a' ls
83
103
104+-- | Move the cursor up by the given number of lines
105+pageUp :: Int -> TextZipper -> TextZipper
106+pageUp pageSize z = if pageSize <= 0
107+ then z
108+ else pageUp (pageSize - 1) $ up z
109+
110+-- | Move the cursor down by the given number of lines
111+pageDown :: Int -> TextZipper -> TextZipper
112+pageDown pageSize z = if pageSize <= 0
113+ then z
114+ else pageDown (pageSize - 1) $ down z
115+
84-- | Move the cursor to the beginning of the current logical line
116-- | Move the cursor to the beginning of the current logical line
85home :: TextZipper -> TextZipper
117home :: TextZipper -> TextZipper
86home (TextZipper lb b a la) = TextZipper lb "" (b <> a) la
118home (TextZipper lb b a la) = TextZipper lb "" (b <> a) la
@@ -135,6 +167,11 @@ deleteLeftWord (TextZipper lb b a la) =
135 (l:ls) -> deleteLeftWord $ TextZipper ls l a la
167 (l:ls) -> deleteLeftWord $ TextZipper ls l a la
136 else TextZipper lb (T.dropWhileEnd (not . isSpace) b') a la
168 else TextZipper lb (T.dropWhileEnd (not . isSpace) b') a la
137
169
170+-- | Insert up to n spaces to get to the next logical column that is a multiple of n
171+tab :: Int -> TextZipper -> TextZipper
172+tab n z@(TextZipper _ b _ _) =
173+ insert (T.replicate (fromEnum $ n - T.length b `mod` max 1 n) " ") z
174+
138-- | The plain text contents of the zipper
175-- | The plain text contents of the zipper
139value :: TextZipper -> Text
176value :: TextZipper -> Text
140value (TextZipper lb b a la) = T.intercalate "\n" $ mconcat [ reverse lb
177value (TextZipper lb b a la) = T.intercalate "\n" $ mconcat [ reverse lb
@@ -156,132 +193,319 @@ fromText = flip insert empty
156data Span tag = Span tag Text
193data Span tag = Span tag Text
157 deriving (Show)
194 deriving (Show)
158
195
196+-- | Text alignment type
197+data TextAlignment =
198+ TextAlignment_Left
199+ | TextAlignment_Right
200+ | TextAlignment_Center
201+ deriving (Eq, Show)
202+
203+-- A map from the index (row) of display line to (fst,snd)
204+-- fst: leading empty spaces from left (may be negative) to adjust for alignment
205+-- snd: the text offset from the beginning of the document
206+-- to the first character of the display line
207+type OffsetMapWithAlignment = Map Int (Int, Int)
208+
209+-- helper type representing a single visual line that may be part of a wrapped logical line
210+data WrappedLine = WrappedLine
211+ { _wrappedLines_text :: Text
212+ , _wrappedLines_hiddenWhitespace :: Bool -- ^ 'True' if this line ends with a deleted whitespace character
213+ , _wrappedLines_offset :: Int -- ^ offset from beginning of line
214+ }
215+ deriving (Eq, Show)
216+
159-- | Information about the document as it is displayed (i.e., post-wrapping)
217-- | Information about the document as it is displayed (i.e., post-wrapping)
160data DisplayLines tag = DisplayLines
218data DisplayLines tag = DisplayLines
161 { _displayLines_spans :: [[Span tag]]
219 { _displayLines_spans :: [[Span tag]]
162โˆ’ , _displayLines_offsetMap :: Map Int Int
220+ , _displayLines_offsetMap :: OffsetMapWithAlignment
163โˆ’ , _displayLines_cursorY :: Int
221+ , _displayLines_cursorPos :: (Int, Int) -- cursor position relative to upper left hand corner
164 }
222 }
165 deriving (Show)
223 deriving (Show)
166
224
225+-- | Split a 'Text' at the given column index. For example
226+--
227+-- > splitAtWidth 3 "แ„€abc" == ("แ„€a", "bc")
228+--
229+-- because the first character has a width of two (see 'charWidth' for more on that).
230+splitAtWidth :: Int -> Text -> (Text, Text)
231+splitAtWidth n t@(Text arr off len)
232+ | n <= 0 = (T.empty, t)
233+ | n >= textWidth t = (t, T.empty)
234+ | otherwise = let k = toLogicalIndex n t
235+ in (text arr off k, text arr (off+k) (len-k))
236+
237+toLogicalIndex :: Int -> Text -> Int
238+toLogicalIndex n' t'@(Text _ _ len') = loop 0 0
239+ where loop !i !cnt
240+ | i >= len' || cnt + w > n' = i
241+ | otherwise = loop (i+d) (cnt + w)
242+ where Iter c d = iter t' i
243+ w = charWidth c
244+
245+-- | Takes the given number of columns of characters. For example
246+--
247+-- > takeWidth 3 "แ„€abc" == "แ„€a"
248+--
249+-- because the first character has a width of 2 (see 'charWidth' for more on that).
250+-- This function will not take a character if its width exceeds the width it seeks to take.
251+takeWidth :: Int -> Text -> Text
252+takeWidth n = fst . splitAtWidth n
253+
254+-- | Drops the given number of columns of characters. For example
255+--
256+-- > dropWidth 2 "แ„€abc" == "abc"
257+--
258+-- because the first character has a width of 2 (see 'charWidth' for more on that).
259+-- This function will not drop a character if its width exceeds the width it seeks to drop.
260+dropWidth :: Int -> Text -> Text
261+dropWidth n = snd . splitAtWidth n
262+
263+-- | Get the display width of a 'Char'. "Full width" and "wide" characters
264+-- take two columns and everything else takes a single column. See
265+-- <https://www.unicode.org/reports/tr11/> for more information
266+-- This is implemented using wcwidth from Vty such that it matches what will
267+-- be displayed on the terminal. Note that this method can change depending
268+-- on how vty is configed. Please see vty documentation for details.
269+charWidth :: Char -> Int
270+charWidth = wcwidth
271+
272+-- | Get the width of the text in a set of 'Span's, taking into account unicode character widths
273+spansWidth :: [Span tag] -> Int
274+spansWidth = sum . map (\(Span _ t) -> textWidth t)
275+
276+-- | Get the length (number of characters) of the text in a set of 'Span's
277+spansLength :: [Span tag] -> Int
278+spansLength = sum . map (\(Span _ t) -> T.length t)
279+
280+-- | Compute the width of some 'Text', taking into account fullwidth
281+-- unicode forms.
282+textWidth :: Text -> Int
283+textWidth t = widthI (stream t)
284+
285+-- | Compute the width of a stream of characters, taking into account
286+-- fullwidth unicode forms.
287+widthI :: Stream Char -> Int
288+widthI (Stream next s0 _len) = loop_length 0 s0
289+ where
290+ loop_length !z s = case next s of
291+ Done -> z
292+ Skip s' -> loop_length z s'
293+ Yield c s' -> loop_length (z + charWidth c) s'
294+{-# INLINE[0] widthI #-}
295+
296+-- | Compute the logical index position of a stream of characters from a visual
297+-- position taking into account fullwidth unicode forms.
298+charIndexAt :: Int -> Stream Char -> Int
299+charIndexAt pos (Stream next s0 _len) = loop_length 0 0 s0
300+ where
301+ loop_length i !z s = case next s of
302+ Done -> i
303+ Skip s' -> loop_length i z s'
304+ Yield c s' -> if w > pos then i else loop_length (i+1) w s' where
305+ w = z + charWidth c
306+{-# INLINE[0] charIndexAt #-}
307+
308+
309+
310+
311+-- | Same as T.words except whitespace characters are included at end (i.e. ["line1 ", ...])
312+-- 'Char's representing white space.
313+wordsWithWhitespace :: Text -> [Text]
314+wordsWithWhitespace t@(Text arr off len) = loop 0 0 False
315+ where
316+ loop !start !n !wasSpace
317+ | n >= len = [Text arr (start+off) (n-start) | not (start == n)]
318+ | isSpace c = loop start (n+d) True
319+ | wasSpace = Text arr (start+off) (n-start) : loop n n False
320+ | otherwise = loop start (n+d) False
321+ where Iter c d = iter t n
322+{-# INLINE wordsWithWhitespace #-}
323+
324+-- | Split words into logical lines, 'True' in the tuple indicates line ends with a whitespace character that got deleted
325+splitWordsAtDisplayWidth :: Int -> [Text] -> [(Text, Bool)]
326+splitWordsAtDisplayWidth maxWidth wwws = reverse $ loop wwws 0 [] where
327+ appendOut :: [(Text,Bool)] -> Text -> Bool -> [(Text,Bool)]
328+ appendOut [] t b = [(t,b)]
329+ appendOut ((t',_):ts') t b = (t'<>t,b) : ts'
330+
331+ -- remove the last whitespace in output
332+ modifyOutForNewLine :: [(Text,Bool)] -> [(Text,Bool)]
333+ modifyOutForNewLine [] = error "should never happen"
334+ modifyOutForNewLine ((t',_):ts) = case T.unsnoc t' of
335+ Nothing -> error "should never happen"
336+ Just (t,lastChar) -> assert (isSpace lastChar) $ (t,True):ts -- assume last char is whitespace
337+
338+ loop :: [Text] -> Int -> [(Text,Bool)] -> [(Text,Bool)]
339+ loop [] _ out = out
340+ loop (x:xs) cumw out = r where
341+ newWidth = textWidth x + cumw
342+ r = if newWidth > maxWidth
343+ then if isSpace $ T.index x (toLogicalIndex (maxWidth - cumw) x)
344+ -- if line runs over but character of splitting is whitespace then split on the whitespace
345+ then let (t1,t2) = splitAtWidth (maxWidth - cumw) x
346+ in loop (T.drop 1 t2:xs) 0 [] <> appendOut out t1 True
347+ else if cumw == 0
348+ -- single word exceeds max width, so just split on the word
349+ then let (t1,t2) = splitAtWidth (maxWidth - cumw) x
350+ in loop (t2:xs) 0 [] <> appendOut out t1 False
351+ -- otherwise start a new line
352+ else loop (x:xs) 0 [] <> modifyOutForNewLine out
353+ else loop xs newWidth $ appendOut out x False
354+
355+
356+
357+-- | Wraps a logical line of text to fit within the given width. The first
358+-- wrapped line is offset by the number of columns provided. Subsequent wrapped
359+-- lines are not.
360+wrapWithOffsetAndAlignment
361+ :: TextAlignment
362+ -> Int -- ^ Maximum width
363+ -> Int -- ^ Offset for first line
364+ -> Text -- ^ Text to be wrapped
365+ -> [WrappedLine] -- (words on that line, hidden space char, offset from beginning of line)
366+wrapWithOffsetAndAlignment _ maxWidth _ _ | maxWidth <= 0 = []
367+wrapWithOffsetAndAlignment alignment maxWidth n txt = assert (n <= maxWidth) r where
368+ -- we pad by offset amount with any non-space character which we will remove later so that no changes need to be made to splitWordsAtDisplayWidth
369+ r' = splitWordsAtDisplayWidth maxWidth $ wordsWithWhitespace ( T.replicate n "." <> txt)
370+ fmapfn (t,b) = case alignment of
371+ TextAlignment_Left -> WrappedLine t b 0
372+ TextAlignment_Right -> WrappedLine t b (maxWidth-l)
373+ TextAlignment_Center -> WrappedLine t b ((maxWidth-l) `div` 2)
374+ where l = textWidth t
375+ r'' = case r' of
376+ [] -> []
377+ (x,b):xs -> (T.drop n x,b):xs
378+ r = fmap fmapfn r''
379+
380+-- converts deleted eol spaces into logical lines
381+eolSpacesToLogicalLines :: [[WrappedLine]] -> [[(Text, Int)]]
382+eolSpacesToLogicalLines = fmap (fmap (\(WrappedLine a _ c) -> (a,c))) . concatMap (L.groupBy (\(WrappedLine _ b _) _ -> not b))
383+
384+offsetMapWithAlignmentInternal :: [[WrappedLine]] -> OffsetMapWithAlignment
385+offsetMapWithAlignmentInternal = offsetMapWithAlignment . eolSpacesToLogicalLines
386+
387+offsetMapWithAlignment
388+ :: [[(Text, Int)]] -- ^ The outer list represents logical lines, inner list represents wrapped lines
389+ -> OffsetMapWithAlignment
390+offsetMapWithAlignment ts = evalState (offsetMap' ts) (0, 0)
391+ where
392+ offsetMap' xs = fmap Map.unions $ forM xs $ \x -> do
393+ maps <- forM x $ \(line,align) -> do
394+ let l = T.length line
395+ (dl, o) <- get
396+ put (dl + 1, o + l)
397+ return $ Map.singleton dl (align, o)
398+ (dl, o) <- get
399+ put (dl, o + 1)
400+ -- add additional offset to last line in wrapped lines (for newline char)
401+ return $ Map.adjust (\(align,_)->(align,o+1)) dl $ Map.unions maps
402+
403+
167-- | Given a width and a 'TextZipper', produce a list of display lines
404-- | Given a width and a 'TextZipper', produce a list of display lines
168-- (i.e., lines of wrapped text) with special attributes applied to
405-- (i.e., lines of wrapped text) with special attributes applied to
169-- certain segments (e.g., the cursor). Additionally, produce the current
406-- certain segments (e.g., the cursor). Additionally, produce the current
170-- y-coordinate of the cursor and a mapping from display line number to text
407-- y-coordinate of the cursor and a mapping from display line number to text
171-- offset
408-- offset
172โˆ’displayLines
409+displayLinesWithAlignment
410+ :: TextAlignment
173โˆ’ :: Int -- ^ Width, used for wrapping
411+ -> Int -- ^ Width, used for wrapping
174 -> tag -- ^ Metadata for normal characters
412 -> tag -- ^ Metadata for normal characters
175 -> tag -- ^ Metadata for the cursor
413 -> tag -- ^ Metadata for the cursor
176 -> TextZipper -- ^ The text input contents and cursor state
414 -> TextZipper -- ^ The text input contents and cursor state
177 -> DisplayLines tag
415 -> DisplayLines tag
178โˆ’displayLines width tag cursorTag (TextZipper lb b a la) =
416+displayLinesWithAlignment alignment width tag cursorTag (TextZipper lb b a la) =
179โˆ’ let linesBefore :: [[Text]] -- The wrapped lines before the cursor line
417+ let linesBefore :: [[WrappedLine]] -- The wrapped lines before the cursor line
180โˆ’ linesBefore = map (wrapWithOffset width 0) $ reverse lb
418+ linesBefore = map (wrapWithOffsetAndAlignment alignment width 0) $ reverse lb
181โˆ’ linesAfter :: [[Text]] -- The wrapped lines after the cursor line
419+ linesAfter :: [[WrappedLine]] -- The wrapped lines after the cursor line
182โˆ’ linesAfter = map (wrapWithOffset width 0) la
420+ linesAfter = map (wrapWithOffsetAndAlignment alignment width 0) la
183โˆ’ offsets :: Map Int Int
421+
184โˆ’ offsets = offsetMap $ mconcat
422+ -- simulate trailing cursor character when computing OffsetMap
423+ afterWithCursor = if T.null a then " " else a
424+ offsets :: OffsetMapWithAlignment
425+ offsets = offsetMapWithAlignmentInternal $ mconcat
185 [ linesBefore
426 [ linesBefore
186โˆ’ , [wrapWithOffset width 0 $ b <> a]
427+ , [wrapWithOffsetAndAlignment alignment width 0 $ b <> afterWithCursor]
187 , linesAfter
428 , linesAfter
188 ]
429 ]
430+ flattenLines = concatMap (fmap _wrappedLines_text)
189โˆ’ spansBefore = map ((:[]) . Span tag) $ concat linesBefore
431+ spansBefore = map ((:[]) . Span tag) $ flattenLines linesBefore
190โˆ’ spansAfter = map ((:[]) . Span tag) $ concat linesAfter
432+ spansAfter = map ((:[]) . Span tag) $ flattenLines linesAfter
191 -- Separate the spans before the cursor into
433 -- Separate the spans before the cursor into
192 -- * spans that are on earlier display lines (though on the same logical line), and
434 -- * spans that are on earlier display lines (though on the same logical line), and
193 -- * spans that are on the same display line
435 -- * spans that are on the same display line
436+
194 (spansCurrentBefore, spansCurLineBefore) = fromMaybe ([], []) $
437 (spansCurrentBefore, spansCurLineBefore) = fromMaybe ([], []) $
195โˆ’ initLast $ map ((:[]) . Span tag) (wrapWithOffset width 0 b)
438+ initLast $ map ((:[]) . Span tag) $ _wrappedLines_text <$> (wrapWithOffsetAndAlignment alignment width 0 b)
196 -- Calculate the number of columns on the cursor's display line before the cursor
439 -- Calculate the number of columns on the cursor's display line before the cursor
197โˆ’ curLineOffset = spansLength spansCurLineBefore
440+ curLineOffset = spansWidth spansCurLineBefore
198 -- Check whether the spans on the current display line are long enough that
441 -- Check whether the spans on the current display line are long enough that
199 -- the cursor has to go to the next line
442 -- the cursor has to go to the next line
200 cursorAfterEOL = curLineOffset == width
443 cursorAfterEOL = curLineOffset == width
444+ cursorCharWidth = case T.uncons a of
445+ Nothing -> 1
446+ Just (c, _) -> charWidth c
447+
201 -- Separate the span after the cursor into
448 -- Separate the span after the cursor into
202 -- * spans that are on the same display line, and
449 -- * spans that are on the same display line, and
203 -- * spans that are on later display lines (though on the same logical line)
450 -- * spans that are on later display lines (though on the same logical line)
451+
204 (spansCurLineAfter, spansCurrentAfter) = fromMaybe ([], []) $
452 (spansCurLineAfter, spansCurrentAfter) = fromMaybe ([], []) $
205 headTail $ case T.uncons a of
453 headTail $ case T.uncons a of
206 Nothing -> [[Span cursorTag " "]]
454 Nothing -> [[Span cursorTag " "]]
207 Just (c, rest) ->
455 Just (c, rest) ->
208โˆ’ let o = if cursorAfterEOL then 1 else curLineOffset + 1
456+ let o = if cursorAfterEOL then cursorCharWidth else curLineOffset + cursorCharWidth
209 cursor = Span cursorTag (T.singleton c)
457 cursor = Span cursorTag (T.singleton c)
210โˆ’ in case map ((:[]) . Span tag) (wrapWithOffset width o rest) of
458+ in case map ((:[]) . Span tag) $ _wrappedLines_text <$> (wrapWithOffsetAndAlignment alignment width o rest) of
211โˆ’ [] -> [[cursor]]
459+ [] -> [[cursor]]
212 (l:ls) -> (cursor : l) : ls
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+
213 in DisplayLines
483 in DisplayLines
214 { _displayLines_spans = concat
484 { _displayLines_spans = concat
215 [ spansBefore
485 [ spansBefore
216 , spansCurrentBefore
486 , spansCurrentBefore
217โˆ’ , if cursorAfterEOL
487+ , curLineSpan
218โˆ’ then [ spansCurLineBefore, spansCurLineAfter ]
219โˆ’ else [ spansCurLineBefore <> spansCurLineAfter ]
220 , spansCurrentAfter
488 , spansCurrentAfter
221 , spansAfter
489 , spansAfter
222 ]
490 ]
223 , _displayLines_offsetMap = offsets
491 , _displayLines_offsetMap = offsets
224โˆ’ , _displayLines_cursorY = sum
492+ , _displayLines_cursorPos = (cursorX, cursorY)
225โˆ’ [ length spansBefore
226โˆ’ , length spansCurrentBefore
227โˆ’ , if cursorAfterEOL then 1 else 0
228โˆ’ ]
229 }
493 }
230 where
494 where
231 initLast :: [a] -> Maybe ([a], a)
495 initLast :: [a] -> Maybe ([a], a)
232 initLast = \case
496 initLast = \case
233 [] -> Nothing
497 [] -> Nothing
234 (x:xs) -> case initLast xs of
498 (x:xs) -> case initLast xs of
235โˆ’ Nothing -> Just ([], x)
499+ Nothing -> Just ([], x)
236 Just (ys, y) -> Just (x:ys, y)
500 Just (ys, y) -> Just (x:ys, y)
237 headTail :: [a] -> Maybe (a, [a])
501 headTail :: [a] -> Maybe (a, [a])
238 headTail = \case
502 headTail = \case
239 [] -> Nothing
503 [] -> Nothing
240 x:xs -> Just (x, xs)
504 x:xs -> Just (x, xs)
241
505
242โˆ’-- | Wraps a logical line of text to fit within the given width. The first
243โˆ’-- wrapped line is offset by the number of columns provided. Subsequent wrapped
244โˆ’-- lines are not.
245โˆ’wrapWithOffset :: Int -> Int -> Text -> [Text]
246โˆ’wrapWithOffset maxWidth _ _ | maxWidth <= 0 = []
247โˆ’wrapWithOffset maxWidth n xs =
248โˆ’ let (firstLine, rest) = T.splitAt (maxWidth - n) xs
249โˆ’ in firstLine : (fmap (T.take maxWidth) . takeWhile (not . T.null) . iterate (T.drop maxWidth) $ rest)
250โˆ’
251โˆ’-- | For a given set of wrapped logical lines, computes a map
252โˆ’-- from display line index to text offset in the original text.
253โˆ’-- This is used to help determine how interactions with the displayed
254โˆ’-- text map back to the original text.
255โˆ’-- For example, given the document @\"AA\\nBBB\\nCCCCCCCC\\n\"@ wrapped to 5 columns,
256โˆ’-- this function will compute the offset in the original document of each character
257โˆ’-- in column 1:
258โˆ’--
259โˆ’-- > AA... (0, 0)
260โˆ’-- > BBB.. (1, 3)
261โˆ’-- > CCCCC (2, 7) -- (this line wraps to the next row)
262โˆ’-- > CCC.. (3, 12)
263โˆ’-- > ..... (4, 16)
264โˆ’offsetMap
265โˆ’ :: [[Text]] -- ^ The outer list represents logical lines, and the
266โˆ’ -- inner list represents the display lines into which
267โˆ’ -- the logical line has been wrapped
268โˆ’ -> Map Int Int -- ^ A map from the index (row) of display line to
269โˆ’ -- the text offset from the beginning of the document
270โˆ’ -- to the first character of the display line
271โˆ’offsetMap ts = evalState (offsetMap' ts) (0, 0)
272โˆ’ where
273โˆ’ offsetMap' xs = fmap Map.unions $ forM xs $ \x -> do
274โˆ’ maps <- forM x $ \line -> do
275โˆ’ let l = T.length line
276โˆ’ (dl, o) <- get
277โˆ’ put (dl + 1, o + l)
278โˆ’ return $ Map.singleton dl o
279โˆ’ (dl, o) <- get
280โˆ’ put (dl, o + 1)
281โˆ’ return $ Map.insert dl (o + 1) $ Map.unions maps
282
506
283-- | Move the cursor of the given 'TextZipper' to the logical position indicated
507-- | Move the cursor of the given 'TextZipper' to the logical position indicated
284โˆ’-- by the given display line coordinates, using the provided 'DisplayLines'
508+-- by the given display line coordinates, using the provided 'DisplayLinesWithAlignment'
285-- information. If the x coordinate is beyond the end of a line, the cursor is
509-- information. If the x coordinate is beyond the end of a line, the cursor is
286-- moved to the end of the line.
510-- moved to the end of the line.
287goToDisplayLinePosition :: Int -> Int -> DisplayLines tag -> TextZipper -> TextZipper
511goToDisplayLinePosition :: Int -> Int -> DisplayLines tag -> TextZipper -> TextZipper
@@ -289,12 +513,33 @@ goToDisplayLinePosition x y dl tz =
289 let offset = Map.lookup y $ _displayLines_offsetMap dl
513 let offset = Map.lookup y $ _displayLines_offsetMap dl
290 in case offset of
514 in case offset of
291 Nothing -> tz
515 Nothing -> tz
292โˆ’ Just o ->
516+ Just (alignOff,o) ->
517+ let
518+ trueX = max 0 (x - alignOff)
293โˆ’ let displayLineLength = case drop y $ _displayLines_spans dl of
519+ moveRight = case drop y $ _displayLines_spans dl of
294โˆ’ [] -> x
520+ [] -> 0
295โˆ’ (s:_) -> spansLength s
521+ (s:_) -> charIndexAt trueX . stream . mconcat . fmap (\(Span _ t) -> t) $ s
296โˆ’ in rightN (o + (min displayLineLength x)) $ top tz
522+ in rightN (o + moveRight) $ top tz
297
523
298โˆ’-- | Get the length of the text in a set of 'Span's
524+-- | Given a width and a 'TextZipper', produce a list of display lines
299โˆ’spansLength :: [Span tag] -> Int
525+-- (i.e., lines of wrapped text) with special attributes applied to
300โˆ’spansLength = sum . map (\(Span _ t) -> T.length t)
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โˆ’30
@@ -1,30 +0,0 @@
1โˆ’{-|
2โˆ’Module: Reflex.Class.Switchable
3โˆ’Description: A class for things that can be switched on the firing of an event
4โˆ’-}
5โˆ’{-# LANGUAGE FlexibleInstances #-}
6โˆ’{-# LANGUAGE FunctionalDependencies #-}
7โˆ’{-# LANGUAGE MultiParamTypeClasses #-}
8โˆ’{-# LANGUAGE UndecidableInstances #-}
9โˆ’module Reflex.Class.Switchable where
10โˆ’
11โˆ’import Control.Monad
12โˆ’import Reflex
13โˆ’
14โˆ’-- | Class representing things that can be switched when the provided event occurs
15โˆ’class Reflex t => Switchable t w | w -> t where
16โˆ’ switching :: MonadHold t m => w -> Event t w -> m w
17โˆ’
18โˆ’instance Reflex t => Switchable t (Event t a) where
19โˆ’ switching = switchHold
20โˆ’
21โˆ’instance Reflex t => Switchable t (Dynamic t a) where
22โˆ’ switching a e = fmap join $ holdDyn a e
23โˆ’
24โˆ’instance Reflex t => Switchable t (Behavior t a) where
25โˆ’ switching = switcher
26โˆ’
27โˆ’instance (Reflex t, Switchable t a, Switchable t b) => Switchable t (a, b) where
28โˆ’ switching (a, b) e = (,)
29โˆ’ <$> (switching a $ fmap fst e)
30โˆ’ <*> (switching b $ fmap snd e)
src/Reflex/NotReady/Class/Orphans.hsdeletedโˆ’17
@@ -1,17 +0,0 @@
1โˆ’{-|
2โˆ’Module: Reflex.NotReady.Class.Orphans
3โˆ’Description: Orphan instances for NotReady
4โˆ’-}
5โˆ’{-# LANGUAGE FlexibleInstances #-}
6โˆ’{-# LANGUAGE MultiParamTypeClasses #-}
7โˆ’{-# LANGUAGE UndecidableInstances #-}
8โˆ’{-# OPTIONS_GHC -fno-warn-orphans #-}
9โˆ’module Reflex.NotReady.Class.Orphans where
10โˆ’
11โˆ’import Control.Monad.Trans
12โˆ’import Control.Monad.Trans.Writer
13โˆ’import Reflex.NotReady.Class
14โˆ’
15โˆ’instance (NotReady t m, Monoid w) => NotReady t (WriterT w m) where
16โˆ’ notReadyUntil = lift . notReadyUntil
17โˆ’ notReady = lift notReady
src/Reflex/Spider/Orphans.hs+3โˆ’4
@@ -2,16 +2,14 @@
2Module: Reflex.Spider.Orphans
2Module: Reflex.Spider.Orphans
3Description: Orphan instances for SpiderTimeline and SpiderHost
3Description: Orphan instances for SpiderTimeline and SpiderHost
4-}
4-}
5โˆ’{-# LANGUAGE FlexibleInstances #-}
6โˆ’{-# LANGUAGE MultiParamTypeClasses #-}
7{-# OPTIONS_GHC -fno-warn-orphans #-}
5{-# OPTIONS_GHC -fno-warn-orphans #-}
8module Reflex.Spider.Orphans where
6module Reflex.Spider.Orphans where
9
7
8+#if MIN_VERSION_reflex(0,6,3)
9+#else
10import Reflex
10import Reflex
11โˆ’import Reflex.NotReady.Class
12import Reflex.Spider.Internal
11import Reflex.Spider.Internal
13
12
14โˆ’-- TODO move this to reflex
15instance NotReady (SpiderTimeline x) (SpiderHost x) where
13instance NotReady (SpiderTimeline x) (SpiderHost x) where
16 notReadyUntil _ = pure ()
14 notReadyUntil _ = pure ()
17 notReady = pure ()
15 notReady = pure ()
@@ -19,3 +17,4 @@ instance NotReady (SpiderTimeline x) (SpiderHost x) where
19instance HasSpiderTimeline x => NotReady (SpiderTimeline x) (PerformEventT (SpiderTimeline x) (SpiderHost x)) where
17instance HasSpiderTimeline x => NotReady (SpiderTimeline x) (PerformEventT (SpiderTimeline x) (SpiderHost x)) where
20 notReadyUntil _ = pure ()
18 notReadyUntil _ = pure ()
21 notReady = pure ()
19 notReady = pure ()
20+#endif
src/Reflex/Vty.hs+14
@@ -6,15 +6,29 @@ License : GPL-3
6Maintainer : maintainer@obsidian.systems
6Maintainer : maintainer@obsidian.systems
7Stability : experimental
7Stability : experimental
8Portability : POSIX
8Portability : POSIX
9+
10+<<./doc/tasks.png>>
11+
9-}
12-}
10module Reflex.Vty
13module Reflex.Vty
11 ( module Reflex
14 ( module Reflex
12 , module Reflex.Vty.Host
15 , module Reflex.Vty.Host
13 , module Reflex.Vty.Widget
16 , module Reflex.Vty.Widget
17+ , module Reflex.Vty.Widget.Box
14 , module Reflex.Vty.Widget.Input
18 , module Reflex.Vty.Widget.Input
19+ , module Reflex.Vty.Widget.Layout
20+ , module Reflex.Vty.Widget.Split
21+ , module Reflex.Vty.Widget.Text
22+ , module Control.Monad.NodeId
15 ) where
23 ) where
16
24
17import Reflex
25import Reflex
18import Reflex.Vty.Host
26import Reflex.Vty.Host
19import Reflex.Vty.Widget
27import Reflex.Vty.Widget
28+import Reflex.Vty.Widget.Box
20import Reflex.Vty.Widget.Input
29import Reflex.Vty.Widget.Input
30+import Reflex.Vty.Widget.Layout
31+import Reflex.Vty.Widget.Split
32+import Reflex.Vty.Widget.Text
33+
34+import Control.Monad.NodeId
src/Reflex/Vty/Host.hs+12โˆ’25
@@ -2,18 +2,6 @@
2Module: Reflex.Vty.Host
2Module: Reflex.Vty.Host
3Description: Scaffolding for running a reflex-vty application
3Description: Scaffolding for running a reflex-vty application
4-}
4-}
5โˆ’{-# LANGUAGE FlexibleContexts #-}
6โˆ’{-# LANGUAGE FlexibleInstances #-}
7โˆ’{-# LANGUAGE LambdaCase #-}
8โˆ’{-# LANGUAGE MultiParamTypeClasses #-}
9โˆ’{-# LANGUAGE RankNTypes #-}
10โˆ’{-# LANGUAGE ScopedTypeVariables #-}
11โˆ’{-# LANGUAGE TypeFamilies #-}
12โˆ’{-# LANGUAGE FunctionalDependencies #-}
13โˆ’{-# LANGUAGE GeneralizedNewtypeDeriving #-}
14โˆ’{-# LANGUAGE ConstraintKinds #-}
15โˆ’{-# LANGUAGE RecursiveDo #-}
16โˆ’
17module Reflex.Vty.Host
5module Reflex.Vty.Host
18 ( VtyApp
6 ( VtyApp
19 , VtyResult(..)
7 , VtyResult(..)
@@ -26,6 +14,7 @@ module Reflex.Vty.Host
26
14
27import Control.Concurrent (forkIO, killThread)
15import Control.Concurrent (forkIO, killThread)
28import Control.Concurrent.Chan (newChan, readChan, writeChan)
16import Control.Concurrent.Chan (newChan, readChan, writeChan)
17+import Control.Exception (onException)
29import Control.Monad (forM, forM_, forever)
18import Control.Monad (forM, forM_, forever)
30import Control.Monad.Fix (MonadFix, fix)
19import Control.Monad.Fix (MonadFix, fix)
31import Control.Monad.IO.Class (liftIO, MonadIO)
20import Control.Monad.IO.Class (liftIO, MonadIO)
@@ -33,13 +22,11 @@ import Control.Monad.Identity (Identity(..))
33import Control.Monad.Primitive (PrimMonad)
22import Control.Monad.Primitive (PrimMonad)
34import Control.Monad.Ref (MonadRef, Ref, readRef)
23import Control.Monad.Ref (MonadRef, Ref, readRef)
35import Data.Dependent.Sum (DSum ((:=>)))
24import Data.Dependent.Sum (DSum ((:=>)))
36โˆ’import Data.IORef (IORef)
25+import Data.IORef (IORef, readIORef)
37โˆ’import Data.IORef (readIORef)
38import Data.Maybe (catMaybes)
26import Data.Maybe (catMaybes)
39
27
40import Reflex
28import Reflex
41import Reflex.Host.Class
29import Reflex.Host.Class
42โˆ’import Reflex.NotReady.Class
43import Reflex.Spider.Orphans ()
30import Reflex.Spider.Orphans ()
44import qualified Graphics.Vty as V
31import qualified Graphics.Vty as V
45import Graphics.Vty (DisplayRegion)
32import Graphics.Vty (DisplayRegion)
@@ -51,7 +38,7 @@ type VtyEvent = V.Event
51-- | The output of a 'VtyApp'.
38-- | The output of a 'VtyApp'.
52data VtyResult t = VtyResult
39data VtyResult t = VtyResult
53 { _vtyResult_picture :: Behavior t V.Picture
40 { _vtyResult_picture :: Behavior t V.Picture
54โˆ’ -- ^ The current vty output. 'runVtyAppWith' samples this value every time an
41+ -- ^ The current vty output. 'runVtyAppWithHandle' samples this value every time an
55 -- event fires and updates the display.
42 -- event fires and updates the display.
56 , _vtyResult_shutdown :: Event t ()
43 , _vtyResult_shutdown :: Event t ()
57 -- ^ An event that requests application termination.
44 -- ^ An event that requests application termination.
@@ -82,7 +69,7 @@ type MonadVtyApp t m =
82type VtyApp t m = MonadVtyApp t m
69type VtyApp t m = MonadVtyApp t m
83 => DisplayRegion
70 => DisplayRegion
84 -- ^ The initial display size (updates to this come as events)
71 -- ^ The initial display size (updates to this come as events)
85โˆ’ -> Event t (V.Event)
72+ -> Event t V.Event
86 -- ^ Vty input events.
73 -- ^ Vty input events.
87 -> m (VtyResult t)
74 -> m (VtyResult t)
88 -- ^ The output of the 'VtyApp'. The application runs in a context that,
75 -- ^ The output of the 'VtyApp'. The application runs in a context that,
@@ -91,14 +78,15 @@ type VtyApp t m = MonadVtyApp t m
91 -- app instantiation ('PostBuild'), and allows actions to be run upon
78 -- app instantiation ('PostBuild'), and allows actions to be run upon
92 -- occurrences of events ('PerformEvent').
79 -- occurrences of events ('PerformEvent').
93
80
94โˆ’-- | Runs a 'VtyApp' in a given 'Vty'.
81+-- | Runs a 'VtyApp' in a given 'Graphics.Vty.Vty'.
95runVtyAppWithHandle
82runVtyAppWithHandle
96 :: V.Vty
83 :: V.Vty
97โˆ’ -- ^ A 'Vty' handle.
84+ -- ^ A 'Graphics.Vty.Vty' handle.
98 -> (forall t m. VtyApp t m)
85 -> (forall t m. VtyApp t m)
99 -- ^ A functional reactive vty application.
86 -- ^ A functional reactive vty application.
100 -> IO ()
87 -> IO ()
101โˆ’runVtyAppWithHandle vty vtyGuest =
88+runVtyAppWithHandle vty vtyGuest = flip onException (V.shutdown vty) $
89+
102 -- We are using the 'Spider' implementation of reflex. Running the host
90 -- We are using the 'Spider' implementation of reflex. Running the host
103 -- allows us to take actions on the FRP timeline. The scoped type signature
91 -- allows us to take actions on the FRP timeline. The scoped type signature
104 -- specifies that our host runs on the Global timeline.
92 -- specifies that our host runs on the Global timeline.
@@ -120,7 +108,7 @@ runVtyAppWithHandle vty vtyGuest =
120 -- processed.
108 -- processed.
121 events <- liftIO newChan
109 events <- liftIO newChan
122
110
123โˆ’ displayRegion0 <- V.displayBounds $ V.outputIface vty
111+ displayRegion0 <- liftIO $ V.displayBounds $ V.outputIface vty
124
112
125 -- Run the vty "guest" application, providing the appropriate context. The
113 -- Run the vty "guest" application, providing the appropriate context. The
126 -- result is a 'VtyResult', and a 'FireCommand' that will be used to
114 -- result is a 'VtyResult', and a 'FireCommand' that will be used to
@@ -184,7 +172,7 @@ runVtyAppWithHandle vty vtyGuest =
184 triggerInvocation = TriggerInvocation ne $ return ()
172 triggerInvocation = TriggerInvocation ne $ return ()
185 -- Write our input event's 'EventTrigger' with the newly created
173 -- Write our input event's 'EventTrigger' with the newly created
186 -- 'TriggerInvocation' value to the queue of events.
174 -- 'TriggerInvocation' value to the queue of events.
187โˆ’ writeChan events $ [triggerRef :=> triggerInvocation]
175+ writeChan events [triggerRef :=> triggerInvocation]
188
176
189 -- The main application loop. We wait for new events, fire those that
177 -- The main application loop. We wait for new events, fire those that
190 -- have subscribers, and update the display. If we detect a shutdown
178 -- have subscribers, and update the display. If we detect a shutdown
@@ -202,13 +190,12 @@ runVtyAppWithHandle vty vtyGuest =
202 if or stop
190 if or stop
203 then liftIO $ do -- If we received a shutdown 'Event'
191 then liftIO $ do -- If we received a shutdown 'Event'
204 killThread nextEventThread -- then stop reading input events and
192 killThread nextEventThread -- then stop reading input events and
205โˆ’ V.shutdown vty -- call the 'Vty's shutdown command.
193+ V.shutdown vty -- call the 'Graphics.Vty.Vty's shutdown command.
206
194
207 else do -- Otherwise, update the display and loop.
195 else do -- Otherwise, update the display and loop.
208 updateVty
196 updateVty
209 loop
197 loop
210 where
198 where
211โˆ’ -- TODO Some part of this is probably general enough to belong in reflex
212 -- | Use the given 'FireCommand' to fire events that have subscribers
199 -- | Use the given 'FireCommand' to fire events that have subscribers
213 -- and call the callback for the 'TriggerInvocation' of each.
200 -- and call the callback for the 'TriggerInvocation' of each.
214 fireEventTriggerRefs
201 fireEventTriggerRefs
@@ -226,7 +213,7 @@ runVtyAppWithHandle vty vtyGuest =
226 liftIO $ forM_ ers $ \(_ :=> TriggerInvocation _ cb) -> cb
213 liftIO $ forM_ ers $ \(_ :=> TriggerInvocation _ cb) -> cb
227 return a
214 return a
228
215
229โˆ’-- | Run a 'VtyApp' with a 'Vty' handle with a standard configuration.
216+-- | Run a 'VtyApp' with a 'Graphics.Vty.Vty' handle with a standard configuration.
230runVtyApp
217runVtyApp
231 :: (forall t m. VtyApp t m)
218 :: (forall t m. VtyApp t m)
232 -> IO ()
219 -> IO ()
src/Reflex/Vty/Widget.hs+488โˆ’478
@@ -2,147 +2,41 @@
2Module: Reflex.Vty.Widget
2Module: Reflex.Vty.Widget
3Description: Basic set of widgets and building blocks for reflex-vty applications
3Description: Basic set of widgets and building blocks for reflex-vty applications
4-}
4-}
5โˆ’{-# LANGUAGE FlexibleContexts #-}
6โˆ’{-# LANGUAGE FlexibleInstances #-}
7โˆ’{-# LANGUAGE FunctionalDependencies #-}
8โˆ’{-# LANGUAGE GeneralizedNewtypeDeriving #-}
9โˆ’{-# LANGUAGE LambdaCase #-}
10โˆ’{-# LANGUAGE MultiParamTypeClasses #-}
11โˆ’{-# LANGUAGE OverloadedStrings #-}
12โˆ’{-# LANGUAGE RankNTypes #-}
13โˆ’{-# LANGUAGE RecursiveDo #-}
14โˆ’{-# LANGUAGE ScopedTypeVariables #-}
15โˆ’{-# LANGUAGE StandaloneDeriving #-}
16โˆ’{-# LANGUAGE TypeFamilies #-}
17โˆ’{-# LANGUAGE UndecidableInstances #-}
5+{-# Language UndecidableInstances #-}
6+
18โˆ’module Reflex.Vty.Widget
7+module Reflex.Vty.Widget where
19โˆ’ ( VtyWidgetCtx(..)
20โˆ’ , VtyWidget(..)
21โˆ’ , VtyWidgetOut(..)
22โˆ’ , runVtyWidget
23โˆ’ , mainWidget
24โˆ’ , mainWidgetWithHandle
25โˆ’ , HasDisplaySize(..)
26โˆ’ , displayWidth
27โˆ’ , displayHeight
28โˆ’ , HasFocus(..)
29โˆ’ , HasVtyInput(..)
30โˆ’ , Region(..)
31โˆ’ , regionSize
32โˆ’ , regionBlankImage
33โˆ’ , Drag(..)
34โˆ’ , drag
35โˆ’ , MouseDown(..)
36โˆ’ , MouseUp(..)
37โˆ’ , mouseDown
38โˆ’ , mouseUp
39โˆ’ , pane
40โˆ’ , modifyImages
41โˆ’ , tellImages
42โˆ’ , tellShutdown
43โˆ’ , splitV
44โˆ’ , splitVDrag
45โˆ’ , box
46โˆ’ , text
47โˆ’ , display
48โˆ’ , hyphenBoxStyle
49โˆ’ , singleBoxStyle
50โˆ’ , roundedBoxStyle
51โˆ’ , thickBoxStyle
52โˆ’ , doubleBoxStyle
53โˆ’ , fill
54โˆ’ , hRule
55โˆ’ ) where
56
8
57import Control.Applicative (liftA2)
9import Control.Applicative (liftA2)
58import Control.Monad.Fix (MonadFix)
10import Control.Monad.Fix (MonadFix)
59โˆ’import Control.Monad.Trans (lift)
11+import Control.Monad.IO.Class (MonadIO)
60โˆ’import Control.Monad.Trans.Reader (ReaderT, runReaderT, asks, ask)
12+import Control.Monad.Morph
13+import Control.Monad.NodeId
14+import Control.Monad.Reader (ReaderT, ask, local, runReaderT)
61โˆ’import Control.Monad.Trans.Writer (WriterT, runWriterT, censor, tell)
15+import Control.Monad.Ref
62โˆ’import Control.Monad.Writer.Adjustable ()
16+import Control.Monad.Trans (MonadTrans, lift)
63โˆ’import Data.Default (Default(..))
64โˆ’import Data.Text (Text)
17+import Data.Set (Set)
65โˆ’import qualified Data.Text as T
18+import qualified Data.Set as Set
66โˆ’import qualified Data.Text.Zipper as TZ
67import Graphics.Vty (Image)
19import Graphics.Vty (Image)
68import qualified Graphics.Vty as V
20import qualified Graphics.Vty as V
69import Reflex
21import Reflex
70โˆ’import Reflex.Class.Switchable
22+import Reflex.Class ()
71โˆ’import Reflex.NotReady.Class
72โˆ’import Reflex.NotReady.Class.Orphans ()
23+import Reflex.Host.Class (MonadReflexCreateTrigger)
73โˆ’
74import Reflex.Vty.Host
24import Reflex.Vty.Host
75
25
76โˆ’-- | The context within which a 'VtyWidget' runs
26+-- * Running a vty application
77โˆ’data VtyWidgetCtx t = VtyWidgetCtx
78โˆ’ { _vtyWidgetCtx_size :: Dynamic t (Int,Int)
79โˆ’ -- ^ The width and height of the region allocated to the widget.
80โˆ’ , _vtyWidgetCtx_focus :: Dynamic t Bool
81โˆ’ -- ^ Whether the widget should behave as if it has focus for keyboard input.
82โˆ’ , _vtyWidgetCtx_input :: Event t VtyEvent
83โˆ’ -- ^ User input events that the widget's parent chooses to share. These will generally
84โˆ’ -- be filtered for relevance:
85โˆ’ -- * Keyboard inputs are restricted to focused widgets
86โˆ’ -- * Mouse inputs are restricted to the region in which the widget resides and are
87โˆ’ -- translated into its internal coordinates.
88โˆ’ }
89โˆ’
90โˆ’-- | The output of a 'VtyWidget'
91โˆ’data VtyWidgetOut t = VtyWidgetOut
92โˆ’ { _vtyWidgetOut_images :: Behavior t [Image]
93โˆ’ -- ^ The visual output of the 'VtyWidget'
94โˆ’ , _vtyWidgetOut_shutdown :: Event t ()
95โˆ’ }
96โˆ’
97โˆ’instance Reflex t => Semigroup (VtyWidgetOut t) where
98โˆ’ wo <> wo' = VtyWidgetOut
99โˆ’ { _vtyWidgetOut_images = _vtyWidgetOut_images wo <> _vtyWidgetOut_images wo'
100โˆ’ , _vtyWidgetOut_shutdown = _vtyWidgetOut_shutdown wo <> _vtyWidgetOut_shutdown wo'
101โˆ’ }
102โˆ’
103โˆ’instance (Reflex t) => Monoid (VtyWidgetOut t) where
104โˆ’ mempty = VtyWidgetOut mempty mempty
105โˆ’ mappend wo wo' = wo <> wo'
106โˆ’
107โˆ’instance Reflex t => Switchable t (VtyWidgetOut t) where
108โˆ’ switching e0 e = do
109โˆ’ shutdown <- switching (_vtyWidgetOut_shutdown e0) $ fmap _vtyWidgetOut_shutdown e
110โˆ’ images <- switching (_vtyWidgetOut_images e0) $ fmap _vtyWidgetOut_images e
111โˆ’ return $ VtyWidgetOut
112โˆ’ { _vtyWidgetOut_images = images
113โˆ’ , _vtyWidgetOut_shutdown = shutdown
114โˆ’ }
115โˆ’
116โˆ’-- | A widget that can read its context and produce image output
117โˆ’newtype VtyWidget t m a = VtyWidget
118โˆ’ { unVtyWidget :: WriterT (VtyWidgetOut t) (ReaderT (VtyWidgetCtx t) m) a
119โˆ’ }
120โˆ’ deriving (Functor, Applicative, Monad, MonadSample t, MonadHold t, MonadFix, NotReady t)
121โˆ’
122โˆ’instance (PostBuild t m, Reflex t) => PostBuild t (VtyWidget t m) where
123โˆ’ getPostBuild = VtyWidget $ lift getPostBuild
124โˆ’
125โˆ’instance (Adjustable t m, MonadHold t m, Reflex t) => Adjustable t (VtyWidget t m) where
126โˆ’ runWithReplace a0 a' = VtyWidget $ runWithReplace (unVtyWidget a0) $ fmap unVtyWidget a'
127โˆ’ traverseIntMapWithKeyWithAdjust f dm0 dm' = VtyWidget $
128โˆ’ traverseIntMapWithKeyWithAdjust (\k v -> unVtyWidget (f k v)) dm0 dm'
129โˆ’ traverseDMapWithKeyWithAdjust f dm0 dm' = VtyWidget $ do
130โˆ’ traverseDMapWithKeyWithAdjust (\k v -> unVtyWidget (f k v)) dm0 dm'
131โˆ’ traverseDMapWithKeyWithAdjustWithMove f dm0 dm' = VtyWidget $ do
132โˆ’ traverseDMapWithKeyWithAdjustWithMove (\k v -> unVtyWidget (f k v)) dm0 dm'
133โˆ’
134โˆ’-- | Runs a 'VtyWidget' with a given context
135โˆ’runVtyWidget
136โˆ’ :: Reflex t
137โˆ’ => VtyWidgetCtx t
138โˆ’ -> VtyWidget t m a
139โˆ’ -> m (a, VtyWidgetOut t)
140โˆ’runVtyWidget ctx w = runReaderT (runWriterT (unVtyWidget w)) ctx
141
27
142โˆ’-- | 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
143mainWidgetWithHandle
29mainWidgetWithHandle
144 :: V.Vty
30 :: V.Vty
31+ -> (forall t m.
145โˆ’ -> (forall t m. MonadVtyApp t m => VtyWidget 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 ()))
146 -> IO ()
40 -> IO ()
147mainWidgetWithHandle vty child =
41mainWidgetWithHandle vty child =
148 runVtyAppWithHandle vty $ \dr0 inp -> do
42 runVtyAppWithHandle vty $ \dr0 inp -> do
@@ -152,71 +46,178 @@ mainWidgetWithHandle vty child =
152 let inp' = fforMaybe inp $ \case
46 let inp' = fforMaybe inp $ \case
153 V.EvResize {} -> Nothing
47 V.EvResize {} -> Nothing
154 x -> Just x
48 x -> Just x
155โˆ’ let ctx = VtyWidgetCtx
49+ (shutdown, images) <- runThemeReader (constant V.defAttr) $
156โˆ’ { _vtyWidgetCtx_size = size
50+ runFocusReader (pure True) $
157โˆ’ , _vtyWidgetCtx_input = inp'
51+ runDisplayRegion (fmap (\(w, h) -> Region 0 0 w h) size) $
158โˆ’ , _vtyWidgetCtx_focus = constDyn True
52+ runImageWriter $
159โˆ’ }
53+ runNodeIdT $
160โˆ’ ((), wo) <- runVtyWidget ctx $ do
54+ runInput inp' $ do
161โˆ’ tellImages . ffor (current size) $ \(w, h) -> [V.charFill V.defAttr ' ' w h]
55+ tellImages . ffor (current size) $ \(w, h) -> [V.charFill V.defAttr ' ' w h]
162โˆ’ child
56+ child
163 return $ VtyResult
57 return $ VtyResult
164โˆ’ { _vtyResult_picture = fmap (V.picForLayers . reverse) (_vtyWidgetOut_images wo)
58+ { _vtyResult_picture = fmap (V.picForLayers . reverse) images
165โˆ’ , _vtyResult_shutdown = _vtyWidgetOut_shutdown wo
59+ , _vtyResult_shutdown = shutdown
166 }
60 }
167
61
62+-- | The output of a vty widget
63+data VtyWidgetOut t = VtyWidgetOut
64+ { _vtyWidgetOut_shutdown :: Event t ()
65+ }
66+
168-- | Like 'mainWidgetWithHandle', but uses a default vty configuration
67-- | Like 'mainWidgetWithHandle', but uses a default vty configuration
169mainWidget
68mainWidget
69+ :: (forall t m.
170โˆ’ :: (forall t m. MonadVtyApp t m => VtyWidget 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 ()))
171 -> IO ()
78 -> IO ()
172mainWidget child = do
79mainWidget child = do
173 vty <- getDefaultVty
80 vty <- getDefaultVty
174 mainWidgetWithHandle vty child
81 mainWidgetWithHandle vty child
175
82
176โˆ’-- | A class for things that know their own display size dimensions
83+-- * Input Events
177โˆ’class (Reflex t, Monad m) => HasDisplaySize t m | m -> t where
178โˆ’ displaySize :: m (Dynamic t (Int, Int))
179โˆ’
180โˆ’instance (Reflex t, Monad m) => HasDisplaySize t (VtyWidget t m) where
181โˆ’ displaySize = VtyWidget . lift $ asks _vtyWidgetCtx_size
182โˆ’
183โˆ’-- | Retrieve the display width (columns)
184โˆ’displayWidth :: HasDisplaySize t m => m (Dynamic t Int)
185โˆ’displayWidth = fmap fst <$> displaySize
186โˆ’
187โˆ’-- | Retrieve the display height (rows)
188โˆ’displayHeight :: HasDisplaySize t m => m (Dynamic t Int)
189โˆ’displayHeight = fmap snd <$> displaySize
190
84
191-- | A class for things that can receive vty events as input
85-- | A class for things that can receive vty events as input
192โˆ’class HasVtyInput t m | m -> t where
86+class HasInput t m | m -> t where
193 input :: m (Event t VtyEvent)
87 input :: m (Event t VtyEvent)
88+ default input :: (f m' ~ m, Monad m', MonadTrans f, HasInput t m') => m (Event t VtyEvent)
89+ input = lift input
90+ -- | User input events that the widget's parent chooses to share. These will generally
91+ -- be filtered for relevance.
92+ localInput :: (Event t VtyEvent -> Event t VtyEvent) -> m a -> m a
93+ default localInput :: (f m' ~ m, Monad m', MFunctor f, HasInput t m') => (Event t VtyEvent -> Event t VtyEvent) -> m a -> m a
94+ localInput f = hoist (localInput f)
95+
96+instance (Reflex t, Monad m) => HasInput t (Input t m) where
97+ input = Input ask
98+ localInput f (Input m) = Input $ local f m
99+
100+-- | A widget that can receive input events. See 'Graphics.Vty.Event'
101+newtype Input t m a = Input
102+ { unInput :: ReaderT (Event t VtyEvent) m a
103+ } deriving
104+ ( Functor
105+ , Applicative
106+ , Monad
107+ , MonadSample t
108+ , MonadHold t
109+ , MonadFix
110+ , MonadIO
111+ , MonadRef
112+ )
113+
114+instance (Adjustable t m, MonadHold t m, Reflex t) => Adjustable t (Input t m) where
115+ runWithReplace a0 a' = Input $ runWithReplace (unInput a0) $ fmap unInput a'
116+ traverseIntMapWithKeyWithAdjust f dm0 dm' = Input $
117+ traverseIntMapWithKeyWithAdjust (\k v -> unInput (f k v)) dm0 dm'
118+ traverseDMapWithKeyWithAdjust f dm0 dm' = Input $ do
119+ traverseDMapWithKeyWithAdjust (\k v -> unInput (f k v)) dm0 dm'
120+ traverseDMapWithKeyWithAdjustWithMove f dm0 dm' = Input $ do
121+ traverseDMapWithKeyWithAdjustWithMove (\k v -> unInput (f k v)) dm0 dm'
122+
123+deriving instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (Input t m)
124+deriving instance NotReady t m => NotReady t (Input t m)
125+deriving instance PerformEvent t m => PerformEvent t (Input t m)
126+deriving instance PostBuild t m => PostBuild t (Input t m)
127+deriving instance TriggerEvent t m => TriggerEvent t (Input t m)
128+instance HasImageWriter t m => HasImageWriter t (Input t m)
129+instance HasDisplayRegion t m => HasDisplayRegion t (Input t m)
130+instance HasFocusReader t m => HasFocusReader t (Input t m)
131+
132+instance MonadTrans (Input t) where
133+ lift f = Input $ lift f
134+
135+instance MFunctor (Input t) where
136+ hoist f = Input . hoist f . unInput
137+
138+instance MonadNodeId m => MonadNodeId (Input t m)
139+
140+-- | Runs an 'Input' with a given context
141+runInput
142+ :: Reflex t
143+ => Event t VtyEvent
144+ -> Input t m a
145+ -> m a
146+runInput e w = runReaderT (unInput w) e
147+
148+-- ** Filtering input
149+
150+-- | Type synonym for a key and modifier combination
151+type KeyCombo = (V.Key, [V.Modifier])
152+
153+-- | Emits an event that fires on a particular key press (without modifiers)
154+key :: (Monad m, Reflex t, HasInput t m) => V.Key -> m (Event t KeyCombo)
155+key = keyCombos . Set.singleton . (,[])
156+
157+-- | Emits an event that fires on particular key presses (without modifiers)
158+keys :: (Monad m, Reflex t, HasInput t m) => [V.Key] -> m (Event t KeyCombo)
159+keys = keyCombos . Set.fromList . fmap (,[])
160+
161+-- | Emit an event that fires whenever the provided key combination occurs
162+keyCombo
163+ :: (Reflex t, Monad m, HasInput t m)
164+ => KeyCombo
165+ -> m (Event t KeyCombo)
166+keyCombo = keyCombos . Set.singleton
167+
168+-- | Emit an event that fires whenever any of the provided key combinations occur
169+keyCombos
170+ :: (Reflex t, Monad m, HasInput t m)
171+ => Set KeyCombo
172+ -> m (Event t KeyCombo)
173+keyCombos ks = do
174+ i <- input
175+ return $ fforMaybe i $ \case
176+ V.EvKey k m -> if Set.member (k, m) ks
177+ then Just (k, m)
178+ else Nothing
179+ _ -> Nothing
194
180
195โˆ’instance (Reflex t, Monad m) => HasVtyInput t (VtyWidget t m) where
181+-- | Filter the keyboard input that a child widget may receive
196โˆ’ input = VtyWidget . lift $ asks _vtyWidgetCtx_input
182+filterKeys :: (Reflex t, HasInput t m) => (KeyCombo -> Bool) -> m a -> m a
197โˆ’
183+filterKeys f x = localInput (ffilter (\case
198โˆ’-- | A class for things that can dynamically gain and lose focus
184+ V.EvKey k mods -> f (k, mods)
199โˆ’class HasFocus t m | m -> t where
185+ _ -> True)) x
200โˆ’ focus :: m (Dynamic t Bool)
186+
201โˆ’
187+-- | Filter mouse input events based on whether they target a particular region
202โˆ’instance (Reflex t, Monad m) => HasFocus t (VtyWidget t m) where
188+-- and translate them to the internal coordinate system of that region.
203โˆ’ focus = VtyWidget . lift $ asks _vtyWidgetCtx_focus
189+--
204โˆ’
190+-- NB: Non-mouse events are passed through unfiltered and unchanged
205โˆ’-- | A class for widgets that can produce images to draw to the display
191+mouseInRegion :: Region -> VtyEvent -> Maybe VtyEvent
206โˆ’class (Reflex t, Monad m) => ImageWriter t m | m -> t where
192+mouseInRegion (Region l t w h) e = case e of
207โˆ’ -- | Send images upstream for rendering
193+ V.EvMouseDown x y btn m -> mouse (\u v -> V.EvMouseDown u v btn m) x y
208โˆ’ tellImages :: Behavior t [Image] -> m ()
194+ V.EvMouseUp x y btn -> mouse (\u v -> V.EvMouseUp u v btn) x y
209โˆ’
195+ _ -> Just e
210โˆ’instance (Reflex t, Monad m) => ImageWriter t (VtyWidget t m) where
196+ where
211โˆ’ tellImages imgs = VtyWidget $ tell (mempty { _vtyWidgetOut_images = imgs })
197+ mouse con x y
212โˆ’
198+ | or [ x < l
213โˆ’-- | A class for things that can shut down the vty application
199+ , y < t
214โˆ’class (Reflex t, Monad m) => Shutdown t m where
200+ , x >= l + w
215โˆ’ -- | Signals that the vty application should shut down
201+ , y >= t + h ] = Nothing
216โˆ’ tellShutdown :: Event t () -> m ()
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
217
219
218โˆ’instance (Reflex t, Monad m) => Shutdown t (VtyWidget t m) where
220+-- * Getting and setting the display region
219โˆ’ tellShutdown sd = VtyWidget $ tell (mempty { _vtyWidgetOut_shutdown = sd })
220
221
221-- | A chunk of the display area
222-- | A chunk of the display area
222data Region = Region
223data Region = Region
@@ -227,14 +228,293 @@ data Region = Region
227 }
228 }
228 deriving (Show, Read, Eq, Ord)
229 deriving (Show, Read, Eq, Ord)
229
230
231+-- | A region that occupies no space.
232+nilRegion :: Region
233+nilRegion = Region 0 0 0 0
234+
230-- | The width and height of a 'Region'
235-- | The width and height of a 'Region'
231regionSize :: Region -> (Int, Int)
236regionSize :: Region -> (Int, Int)
232regionSize (Region _ _ w h) = (w, h)
237regionSize (Region _ _ w h) = (w, h)
233
238
234-- | Produces an 'Image' that fills a region with space characters
239-- | Produces an 'Image' that fills a region with space characters
235โˆ’regionBlankImage :: Region -> Image
240+regionBlankImage :: V.Attr -> Region -> Image
236โˆ’regionBlankImage r@(Region _ _ width height) =
241+regionBlankImage attr r@(Region _ _ width height) =
237โˆ’ withinImage r $ V.charFill V.defAttr ' ' 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
238
518
239-- | Translates and crops an 'Image' so that it is contained by
519-- | Translates and crops an 'Image' so that it is contained by
240-- the given 'Region'.
520-- the given 'Region'.
@@ -246,7 +526,17 @@ withinImage (Region left top width height)
246 | width < 0 || height < 0 = withinImage (Region left top 0 0)
526 | width < 0 || height < 0 = withinImage (Region left top 0 0)
247 | otherwise = V.translate left top . V.crop width height
527 | otherwise = V.translate left top . V.crop width height
248
528
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+
249โˆ’-- | Low-level widget combinator that runs a child 'VtyWidget' within
539+-- | Low-level widget combinator that runs a child widget within
250-- a given region and context. This widget filters and modifies the input
540-- a given region and context. This widget filters and modifies the input
251-- that the child widget receives such that:
541-- that the child widget receives such that:
252-- * unfocused widgets receive no key events
542-- * unfocused widgets receive no key events
@@ -254,298 +544,18 @@ withinImage (Region left top width height)
254-- * mouse inputs inside the region have their coordinates translated such
544-- * mouse inputs inside the region have their coordinates translated such
255-- that (0,0) is the top-left corner of the region
545-- that (0,0) is the top-left corner of the region
256pane
546pane
257โˆ’ :: (Reflex t, Monad m)
547+ :: (Reflex t, Monad m, HasInput t m, HasImageWriter t m, HasDisplayRegion t m, HasFocusReader t m)
258โˆ’ => Dynamic t Region -- ^ Region into which we should draw the widget (in coordinates relative to our own)
548+ => Dynamic t Region
259 -> Dynamic t Bool -- ^ Whether the widget should be focused when the parent is.
549 -> Dynamic t Bool -- ^ Whether the widget should be focused when the parent is.
260โˆ’ -> VtyWidget t m a
550+ -> m a
261โˆ’ -> VtyWidget t m a
551+ -> m a
262โˆ’pane reg foc child = VtyWidget $ do
552+pane dr foc child = localRegion (const dr) $
263โˆ’ ctx <- lift ask
553+ mapImages (imagesInRegion $ current dr) $
264โˆ’ let ctx' = VtyWidgetCtx
554+ localFocus (const foc) $
265โˆ’ { _vtyWidgetCtx_input = leftmost -- TODO: think about this leftmost more.
555+ inputInFocusedRegion >>= \e -> localInput (const e) child
266โˆ’ [ fmapMaybe id $
556+
267โˆ’ attachWith (\(r,f) e -> filterInput r f e)
557+-- * Misc
268โˆ’ (liftA2 (,) (current reg) (current foc))
558+
269โˆ’ (_vtyWidgetCtx_input ctx)
559+-- | A widget that draws nothing
270โˆ’ ]
271โˆ’ , _vtyWidgetCtx_focus = liftA2 (&&) (_vtyWidgetCtx_focus ctx) foc
272โˆ’ , _vtyWidgetCtx_size = fmap regionSize reg }
273โˆ’ (result, wo) <- lift . lift $ runVtyWidget ctx' child
274โˆ’ let images = _vtyWidgetOut_images wo
275โˆ’ images' = liftA2 (\r is -> map (withinImage r) is) (current reg) images
276โˆ’ wo' = wo { _vtyWidgetOut_images = images' }
277โˆ’ tell wo'
278โˆ’ return result
279โˆ’ where
280โˆ’ filterInput :: Region -> Bool -> VtyEvent -> Maybe VtyEvent
281โˆ’ filterInput (Region l t w h) focused e = case e of
282โˆ’ V.EvKey _ _ | not focused -> Nothing
283โˆ’ V.EvMouseDown x y btn m -> mouse (\u v -> V.EvMouseDown u v btn m) x y
284โˆ’ V.EvMouseUp x y btn -> mouse (\u v -> V.EvMouseUp u v btn) x y
285โˆ’ _ -> Just e
286โˆ’ where
287โˆ’ mouse con x y
288โˆ’ | or [ x < l
289โˆ’ , y < t
290โˆ’ , x >= l + w
291โˆ’ , y >= t + h ] = Nothing
292โˆ’ | otherwise =
293โˆ’ Just (con (x - l) (y - t))
294โˆ’
295โˆ’-- | Information about a drag operation
296โˆ’data Drag = Drag
297โˆ’ { _drag_from :: (Int, Int) -- ^ Where the drag began
298โˆ’ , _drag_to :: (Int, Int) -- ^ Where the mouse currently is
299โˆ’ , _drag_button :: V.Button -- ^ Which mouse button is dragging
300โˆ’ , _drag_modifiers :: [V.Modifier] -- ^ What modifiers are held
301โˆ’ , _drag_end :: Bool -- ^ Whether the drag ended (the mouse button was released)
302โˆ’ }
303โˆ’ deriving (Eq, Ord, Show)
304โˆ’
305โˆ’-- | Converts raw vty mouse drag events into an event stream of 'Drag's
306โˆ’drag
307โˆ’ :: (Reflex t, MonadFix m, MonadHold t m)
308โˆ’ => V.Button
309โˆ’ -> VtyWidget t m (Event t Drag)
310โˆ’drag btn = do
311โˆ’ inp <- input
312โˆ’ let f :: Maybe Drag -> V.Event -> Maybe Drag
313โˆ’ f Nothing = \case
314โˆ’ V.EvMouseDown x y btn' mods
315โˆ’ | btn == btn' -> Just $ Drag (x,y) (x,y) btn' mods False
316โˆ’ | otherwise -> Nothing
317โˆ’ _ -> Nothing
318โˆ’ f (Just (Drag from _ _ mods end)) = \case
319โˆ’ V.EvMouseDown x y btn' mods'
320โˆ’ | end -> Just $ Drag (x,y) (x,y) btn' mods' False
321โˆ’ | btn == btn' -> Just $ Drag from (x,y) btn mods' False
322โˆ’ | otherwise -> Nothing -- Ignore other buttons.
323โˆ’ V.EvMouseUp x y (Just btn')
324โˆ’ | end -> Nothing
325โˆ’ | btn == btn' -> Just $ Drag from (x,y) btn mods True
326โˆ’ | otherwise -> Nothing
327โˆ’ V.EvMouseUp x y Nothing -- Terminal doesn't specify mouse up button,
328โˆ’ -- assume it's the right one.
329โˆ’ | end -> Nothing
330โˆ’ | otherwise -> Just $ Drag from (x,y) btn mods True
331โˆ’ _ -> Nothing
332โˆ’ rec let newDrag = attachWithMaybe f (current dragD) inp
333โˆ’ dragD <- holdDyn Nothing $ Just <$> newDrag
334โˆ’ return (fmapMaybe id $ updated dragD)
335โˆ’
336โˆ’-- | Mouse down events for a particular mouse button
337โˆ’mouseDown
338โˆ’ :: (Reflex t, Monad m)
339โˆ’ => V.Button
340โˆ’ -> VtyWidget t m (Event t MouseDown)
341โˆ’mouseDown btn = do
342โˆ’ i <- input
343โˆ’ return $ fforMaybe i $ \case
344โˆ’ V.EvMouseDown x y btn' mods -> if btn == btn'
345โˆ’ then Just $ MouseDown btn' (x, y) mods
346โˆ’ else Nothing
347โˆ’ _ -> Nothing
348โˆ’
349โˆ’-- | Mouse up events for a particular mouse button
350โˆ’mouseUp
351โˆ’ :: (Reflex t, Monad m)
352โˆ’ => VtyWidget t m (Event t MouseUp)
353โˆ’mouseUp = do
354โˆ’ i <- input
355โˆ’ return $ fforMaybe i $ \case
356โˆ’ V.EvMouseUp x y btn' -> Just $ MouseUp btn' (x, y)
357โˆ’ _ -> Nothing
358โˆ’
359โˆ’-- | Information about a mouse down event
360โˆ’data MouseDown = MouseDown
361โˆ’ { _mouseDown_button :: V.Button
362โˆ’ , _mouseDown_coordinates :: (Int, Int)
363โˆ’ , _mouseDown_modifiers :: [V.Modifier]
364โˆ’ }
365โˆ’ deriving (Eq, Ord, Show)
366โˆ’
367โˆ’-- | Information about a mouse up event
368โˆ’data MouseUp = MouseUp
369โˆ’ { _mouseUp_button :: Maybe V.Button
370โˆ’ , _mouseUp_coordinates :: (Int, Int)
371โˆ’ }
372โˆ’ deriving (Eq, Ord, Show)
373โˆ’
374โˆ’-- | A plain split of the available space into vertically stacked panes.
375โˆ’-- No visual separator is built in here.
376โˆ’splitV :: (Reflex t, Monad m)
377โˆ’ => Dynamic t (Int -> Int)
378โˆ’ -- ^ Function used to determine size of first pane based on available size
379โˆ’ -> Dynamic t (Bool, Bool)
380โˆ’ -- ^ How to focus the two sub-panes, given that we are focused.
381โˆ’ -> VtyWidget t m a
382โˆ’ -- ^ Widget for first pane
383โˆ’ -> VtyWidget t m b
384โˆ’ -- ^ Widget for second pane
385โˆ’ -> VtyWidget t m (a,b)
386โˆ’splitV sizeFunD focD wA wB = do
387โˆ’ sz <- displaySize
388โˆ’ let regA = (\f (w,h) -> Region 0 0 w (f h)) <$> sizeFunD <*> sz
389โˆ’ regB = (\(w,h) (Region _ _ _ hA) -> Region 0 hA w (h - hA)) <$> sz <*> regA
390โˆ’ ra <- pane regA (fst <$> focD) wA
391โˆ’ rb <- pane regB (snd <$> focD) wB
392โˆ’ return (ra,rb)
393โˆ’
394โˆ’-- | A split of the available space into two parts with a draggable separator.
395โˆ’-- Starts with half the space allocated to each, and the first pane has focus.
396โˆ’-- Clicking in a pane switches focus.
397โˆ’splitVDrag :: (Reflex t, MonadFix m, MonadHold t m)
398โˆ’ => VtyWidget t m ()
399โˆ’ -> VtyWidget t m a
400โˆ’ -> VtyWidget t m b
401โˆ’ -> VtyWidget t m (a,b)
402โˆ’splitVDrag wS wA wB = do
403โˆ’ sz <- displaySize
404โˆ’ (_, h0) <- sample $ current sz
405โˆ’ dragE <- drag V.BLeft
406โˆ’ let splitter0 = h0 `div` 2
407โˆ’ rec splitterCheckpoint <- holdDyn splitter0 $ leftmost [fst <$> ffilter snd dragSplitter, resizeSplitter]
408โˆ’ splitterPos <- holdDyn splitter0 $ leftmost [fst <$> dragSplitter, resizeSplitter]
409โˆ’ splitterFrac <- holdDyn ((1::Double) / 2) $ ffor (attach (current sz) (fst <$> dragSplitter)) $ \((_,h),x) ->
410โˆ’ fromIntegral x / fromIntegral h
411โˆ’ let dragSplitter = fforMaybe (attach (current splitterCheckpoint) dragE) $
412โˆ’ \(splitterY, Drag (_, fromY) (_, toY) _ _ end) ->
413โˆ’ if splitterY == fromY then Just (toY, end) else Nothing
414โˆ’ regA = (\(w,_) sp -> Region 0 0 w sp) <$> sz <*> splitterPos
415โˆ’ regS = (\(w,_) sp -> Region 0 sp w 1) <$> sz <*> splitterPos
416โˆ’ regB = (\(w,h) sp -> Region 0 (sp + 1) w (h - sp - 1)) <$> sz <*> splitterPos
417โˆ’ resizeSplitter = ffor (attach (current splitterFrac) (updated sz)) $
418โˆ’ \(frac, (_,h)) -> round (frac * fromIntegral h)
419โˆ’ focA <- holdDyn True $ leftmost
420โˆ’ [ True <$ mA
421โˆ’ , False <$ mB
422โˆ’ ]
423โˆ’ (mA, rA) <- pane regA focA $ withMouseDown wA
424โˆ’ pane regS (pure False) wS
425โˆ’ (mB, rB) <- pane regB (not <$> focA) $ withMouseDown wB
426โˆ’ return (rA, rB)
427โˆ’ where
428โˆ’ withMouseDown x = do
429โˆ’ m <- mouseDown V.BLeft
430โˆ’ x' <- x
431โˆ’ return (m, x')
432โˆ’
433โˆ’-- | Fill the background with a particular character.
434โˆ’fill :: (Reflex t, Monad m) => Char -> VtyWidget t m ()
435โˆ’fill c = do
436โˆ’ sz <- displaySize
437โˆ’ let fillImg = ffor (current sz) $ \(w,h) -> [V.charFill V.defAttr c w h]
438โˆ’ tellImages fillImg
439โˆ’
440โˆ’-- | Fill the background with the bottom
441โˆ’hRule :: (Reflex t, Monad m) => BoxStyle -> VtyWidget t m ()
442โˆ’hRule boxStyle = fill (_boxStyle_s boxStyle)
443โˆ’
444โˆ’-- | Transform the images produces by a widget
445โˆ’modifyImages
446โˆ’ :: (Reflex t, MonadHold t m, MonadFix m)
447โˆ’ => Behavior t ([Image] -> [Image])
448โˆ’ -> VtyWidget t m a
449โˆ’ -> VtyWidget t m a
450โˆ’modifyImages f (VtyWidget w) = VtyWidget $ flip censor w $ \wo ->
451โˆ’ wo { _vtyWidgetOut_images = f <*> (_vtyWidgetOut_images wo) }
452โˆ’
453โˆ’-- | Defines a set of symbols to use to draw the outlines of boxes
454โˆ’-- C.f. https://en.wikipedia.org/wiki/Box-drawing_character
455โˆ’data BoxStyle = BoxStyle
456โˆ’ { _boxStyle_nw :: Char
457โˆ’ , _boxStyle_n :: Char
458โˆ’ , _boxStyle_ne :: Char
459โˆ’ , _boxStyle_e :: Char
460โˆ’ , _boxStyle_se :: Char
461โˆ’ , _boxStyle_s :: Char
462โˆ’ , _boxStyle_sw :: Char
463โˆ’ , _boxStyle_w :: Char
464โˆ’ }
465โˆ’
466โˆ’instance Default BoxStyle where
467โˆ’ def = singleBoxStyle
468โˆ’
469โˆ’-- | A box style that uses hyphens and pipe characters. Doesn't handle
470โˆ’-- corners very well.
471โˆ’hyphenBoxStyle :: BoxStyle
472โˆ’hyphenBoxStyle = BoxStyle '-' '-' '-' '|' '-' '-' '-' '|'
473โˆ’
474โˆ’-- | A single line box style
475โˆ’singleBoxStyle :: BoxStyle
476โˆ’singleBoxStyle = BoxStyle 'โ”Œ' 'โ”€' 'โ”' 'โ”‚' 'โ”˜' 'โ”€' 'โ””' 'โ”‚'
477โˆ’
478โˆ’-- | A thick single line box style
479โˆ’thickBoxStyle :: BoxStyle
480โˆ’thickBoxStyle = BoxStyle 'โ”' 'โ”' 'โ”“' 'โ”ƒ' 'โ”›' 'โ”' 'โ”—' 'โ”ƒ'
481โˆ’
482โˆ’-- | A double line box style
483โˆ’doubleBoxStyle :: BoxStyle
484โˆ’doubleBoxStyle = BoxStyle 'โ•”' 'โ•' 'โ•—' 'โ•‘' 'โ•' 'โ•' 'โ•š' 'โ•‘'
485โˆ’
486โˆ’-- | A single line box style with rounded corners
487โˆ’roundedBoxStyle :: BoxStyle
488โˆ’roundedBoxStyle = BoxStyle 'โ•ญ' 'โ”€' 'โ•ฎ' 'โ”‚' 'โ•ฏ' 'โ”€' 'โ•ฐ' 'โ”‚'
489โˆ’
490โˆ’-- | Draws a box in the provided style and a child widget inside of that box
491โˆ’box :: (Monad m, Reflex t)
560+blank :: Monad m => m ()
492โˆ’ => BoxStyle
561+blank = return ()
493โˆ’ -> VtyWidget t m a
494โˆ’ -> VtyWidget t m a
495โˆ’box style child = do
496โˆ’ sz <- displaySize
497โˆ’ let boxReg = ffor (current sz) $ \(w,h) -> Region 0 0 w h
498โˆ’ innerReg = ffor sz $ \(w,h) -> Region 1 1 (w - 2) (h - 2)
499โˆ’ tellImages (fmap boxImages boxReg)
500โˆ’ tellImages (fmap (\r -> [regionBlankImage r]) (current innerReg))
501โˆ’ pane innerReg (pure True) child
502โˆ’ where
503โˆ’ boxImages :: Region -> [Image]
504โˆ’ boxImages (Region left top width height) =
505โˆ’ let right = left + width - 1
506โˆ’ bottom = top + height - 1
507โˆ’ sides =
508โˆ’ [ withinImage (Region (left + 1) top (width - 2) 1) $
509โˆ’ V.charFill V.defAttr (_boxStyle_n style) (width - 2) 1
510โˆ’ , withinImage (Region right (top + 1) 1 (height - 2)) $
511โˆ’ V.charFill V.defAttr (_boxStyle_e style) 1 (height - 2)
512โˆ’ , withinImage (Region (left + 1) bottom (width - 2) 1) $
513โˆ’ V.charFill V.defAttr (_boxStyle_s style) (width - 2) 1
514โˆ’ , withinImage (Region left (top + 1) 1 (height - 2)) $
515โˆ’ V.charFill V.defAttr (_boxStyle_w style) 1 (height - 2)
516โˆ’ ]
517โˆ’ corners =
518โˆ’ [ withinImage (Region left top 1 1) $
519โˆ’ V.char V.defAttr (_boxStyle_nw style)
520โˆ’ , withinImage (Region right top 1 1) $
521โˆ’ V.char V.defAttr (_boxStyle_ne style)
522โˆ’ , withinImage (Region right bottom 1 1) $
523โˆ’ V.char V.defAttr (_boxStyle_se style)
524โˆ’ , withinImage (Region left bottom 1 1) $
525โˆ’ V.char V.defAttr (_boxStyle_sw style)
526โˆ’ ]
527โˆ’ in sides ++ if width > 1 && height > 1 then corners else []
528โˆ’
529โˆ’-- | Renders text, wrapped to the container width
530โˆ’text
531โˆ’ :: (Reflex t, Monad m)
532โˆ’ => Behavior t Text
533โˆ’ -> VtyWidget t m ()
534โˆ’text msg = do
535โˆ’ dw <- displayWidth
536โˆ’ let img = (\w s -> [wrapText w V.defAttr s]) <$> current dw <*> msg
537โˆ’ tellImages img
538โˆ’ where
539โˆ’ wrapText maxWidth attrs = V.vertCat
540โˆ’ . concatMap (fmap (V.string attrs . T.unpack) . TZ.wrapWithOffset maxWidth 0)
541โˆ’ . T.split (=='\n')
542โˆ’
543โˆ’-- | Renders any behavior whose value can be converted to
544โˆ’-- 'String' as text
545โˆ’display
546โˆ’ :: (Reflex t, Monad m, Show a)
547โˆ’ => Behavior t a
548โˆ’ -> VtyWidget t m ()
549โˆ’display a = text $ T.pack . show <$> a
550โˆ’
551โˆ’
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+151โˆ’2
@@ -3,7 +3,156 @@ Module: Reflex.Vty.Widget.Input
3Description: User input widgets for reflex-vty
3Description: User input widgets for reflex-vty
4-}
4-}
5module Reflex.Vty.Widget.Input
5module Reflex.Vty.Widget.Input
6+ ( module Export
6โˆ’ ( module Reflex.Vty.Widget.Input.Text
7+ , module Reflex.Vty.Widget.Input
7 ) where
8 ) where
8
9
10+
11+import Reflex.Vty.Widget.Input.Mouse as Export
9โˆ’import Reflex.Vty.Widget.Input.Text
12+import Reflex.Vty.Widget.Input.Text as Export
13+
14+import Control.Monad (join)
15+import Control.Monad.Fix (MonadFix)
16+import Data.Default (Default(..))
17+import Data.Text (Text)
18+import qualified Graphics.Vty as V
19+import Reflex
20+import Reflex.Vty.Widget
21+import Reflex.Vty.Widget.Box
22+import Reflex.Vty.Widget.Text
23+
24+-- * Buttons
25+
26+-- | Configuration options for the 'button' widget
27+data ButtonConfig t = ButtonConfig
28+ { _buttonConfig_boxStyle :: Behavior t BoxStyle
29+ , _buttonConfig_focusStyle :: Behavior t BoxStyle
30+ }
31+
32+instance Reflex t => Default (ButtonConfig t) where
33+ def = ButtonConfig (pure singleBoxStyle) (pure thickBoxStyle)
34+
35+-- | A button widget that contains a sub-widget
36+button
37+ :: (Reflex t, Monad m, HasFocusReader t m, HasTheme t m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m)
38+ => ButtonConfig t
39+ -> m ()
40+ -> m (Event t ())
41+button cfg child = do
42+ f <- focus
43+ let style = do
44+ isFocused <- current f
45+ if isFocused
46+ then _buttonConfig_focusStyle cfg
47+ else _buttonConfig_boxStyle cfg
48+ box style child
49+ m <- mouseUp
50+ k <- key V.KEnter
51+ return $ leftmost [() <$ k, () <$ m]
52+
53+-- | A button widget that displays text that can change
54+textButton
55+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasFocusReader t m, HasTheme t m, HasImageWriter t m, HasInput t m)
56+ => ButtonConfig t
57+ -> Behavior t Text
58+ -> m (Event t ())
59+textButton cfg = button cfg . text -- TODO Centering etc.
60+
61+-- | A button widget that displays a static bit of text
62+textButtonStatic
63+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasFocusReader t m, HasTheme t m, HasImageWriter t m, HasInput t m)
64+ => ButtonConfig t
65+ -> Text
66+ -> m (Event t ())
67+textButtonStatic cfg = textButton cfg . pure
68+
69+-- * Links
70+
71+-- | A clickable link widget
72+link
73+ :: (Reflex t, Monad m, HasDisplayRegion t m, HasImageWriter t m, HasInput t m, HasTheme t m)
74+ => Behavior t Text
75+ -> m (Event t MouseUp)
76+link t = do
77+ bt <- theme
78+ let cfg = RichTextConfig
79+ { _richTextConfig_attributes = fmap (\attr -> V.withStyle attr V.underline) bt
80+ }
81+ richText cfg t
82+ mouseUp
83+
84+-- | A clickable link widget with a static label
85+linkStatic
86+ :: (Reflex t, Monad m, HasImageWriter t m, HasDisplayRegion t m, HasInput t m, HasTheme t m)
87+ => Text
88+ -> m (Event t MouseUp)
89+linkStatic = link . pure
90+
91+-- * Checkboxes
92+
93+-- | Characters used to render checked and unchecked textboxes
94+data CheckboxStyle = CheckboxStyle
95+ { _checkboxStyle_unchecked :: Text
96+ , _checkboxStyle_checked :: Text
97+ }
98+
99+instance Default CheckboxStyle where
100+ def = checkboxStyleTick
101+
102+-- | This checkbox style uses an "x" to indicate the checked state
103+checkboxStyleX :: CheckboxStyle
104+checkboxStyleX = CheckboxStyle
105+ { _checkboxStyle_unchecked = "[ ]"
106+ , _checkboxStyle_checked = "[x]"
107+ }
108+
109+-- | This checkbox style uses a unicode tick mark to indicate the checked state
110+checkboxStyleTick :: CheckboxStyle
111+checkboxStyleTick = CheckboxStyle
112+ { _checkboxStyle_unchecked = "[ ]"
113+ , _checkboxStyle_checked = "[โœ“]"
114+ }
115+
116+-- | Configuration options for a checkbox
117+data CheckboxConfig t = CheckboxConfig
118+ { _checkboxConfig_checkboxStyle :: Behavior t CheckboxStyle
119+ -- TODO DELETE and use HasTheme instead
120+ , _checkboxConfig_attributes :: Behavior t V.Attr
121+ , _checkboxConfig_setValue :: Event t Bool
122+ }
123+
124+instance (Reflex t) => Default (CheckboxConfig t) where
125+ def = CheckboxConfig
126+ { _checkboxConfig_checkboxStyle = pure def
127+ , _checkboxConfig_attributes = pure V.defAttr
128+ , _checkboxConfig_setValue = never
129+ }
130+
131+-- | A checkbox widget
132+checkbox
133+ :: (MonadHold t m, MonadFix m, Reflex t, HasInput t m, HasDisplayRegion t m, HasImageWriter t m, HasFocusReader t m, HasTheme t m)
134+ => CheckboxConfig t
135+ -> Bool
136+ -> m (Dynamic t Bool)
137+checkbox cfg v0 = do
138+ md <- mouseDown V.BLeft
139+ mu <- mouseUp
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
148+ depressed <- hold mempty $ leftmost
149+ [ bold <$ md
150+ , mempty <$ mu
151+ ]
152+ let focused = ffor (current f) $ \x -> if x then bold else mempty
153+ let attrs = mconcat <$> sequence [_checkboxConfig_attributes cfg, depressed, focused]
154+ richText (RichTextConfig attrs) $ join . current $ ffor v $ \checked ->
155+ if checked
156+ then _checkboxStyle_checked <$> _checkboxConfig_checkboxStyle cfg
157+ else _checkboxStyle_unchecked <$> _checkboxConfig_checkboxStyle cfg
158+ return v
src/Reflex/Vty/Widget/Input/Mouse.hsnew file+106
@@ -0,0 +1,106 @@
1+{-|
2+ Description: Mouse clicks, drags, and scrolls
3+-}
4+module Reflex.Vty.Widget.Input.Mouse where
5+
6+import Control.Monad.Fix
7+import qualified Graphics.Vty as V
8+import Reflex
9+import Reflex.Vty.Widget
10+
11+-- | Information about a drag operation
12+data Drag = Drag
13+ { _drag_from :: (Int, Int) -- ^ Where the drag began
14+ , _drag_to :: (Int, Int) -- ^ Where the mouse currently is
15+ , _drag_button :: V.Button -- ^ Which mouse button is dragging
16+ , _drag_modifiers :: [V.Modifier] -- ^ What modifiers are held
17+ , _drag_end :: Bool -- ^ Whether the drag ended (the mouse button was released)
18+ }
19+ deriving (Eq, Ord, Show)
20+
21+-- | Converts raw vty mouse drag events into an event stream of 'Drag's
22+drag
23+ :: (Reflex t, MonadFix m, MonadHold t m, HasInput t m)
24+ => V.Button
25+ -> m (Event t Drag)
26+drag btn = do
27+ inp <- input
28+ let f :: Maybe Drag -> V.Event -> Maybe Drag
29+ f Nothing = \case
30+ V.EvMouseDown x y btn' mods
31+ | btn == btn' -> Just $ Drag (x,y) (x,y) btn' mods False
32+ | otherwise -> Nothing
33+ _ -> Nothing
34+ f (Just (Drag from _ _ mods end)) = \case
35+ V.EvMouseDown x y btn' mods'
36+ | end && btn == btn' -> Just $ Drag (x,y) (x,y) btn' mods' False
37+ | btn == btn' -> Just $ Drag from (x,y) btn mods' False
38+ | otherwise -> Nothing -- Ignore other buttons.
39+ V.EvMouseUp x y (Just btn')
40+ | end -> Nothing
41+ | btn == btn' -> Just $ Drag from (x,y) btn mods True
42+ | otherwise -> Nothing
43+ V.EvMouseUp x y Nothing -- Terminal doesn't specify mouse up button,
44+ -- assume it's the right one.
45+ | end -> Nothing
46+ | otherwise -> Just $ Drag from (x,y) btn mods True
47+ _ -> Nothing
48+ rec let newDrag = attachWithMaybe f (current dragD) inp
49+ dragD <- holdDyn Nothing $ Just <$> newDrag
50+ return (fmapMaybe id $ updated dragD)
51+
52+-- | Mouse down events for a particular mouse button
53+mouseDown
54+ :: (Reflex t, Monad m, HasInput t m)
55+ => V.Button
56+ -> m (Event t MouseDown)
57+mouseDown btn = do
58+ i <- input
59+ return $ fforMaybe i $ \case
60+ V.EvMouseDown x y btn' mods -> if btn == btn'
61+ then Just $ MouseDown btn' (x, y) mods
62+ else Nothing
63+ _ -> Nothing
64+
65+-- | Mouse up events for a particular mouse button
66+mouseUp
67+ :: (Reflex t, Monad m, HasInput t m)
68+ => m (Event t MouseUp)
69+mouseUp = do
70+ i <- input
71+ return $ fforMaybe i $ \case
72+ V.EvMouseUp x y btn' -> Just $ MouseUp btn' (x, y)
73+ _ -> Nothing
74+
75+-- | Information about a mouse down event
76+data MouseDown = MouseDown
77+ { _mouseDown_button :: V.Button
78+ , _mouseDown_coordinates :: (Int, Int)
79+ , _mouseDown_modifiers :: [V.Modifier]
80+ }
81+ deriving (Eq, Ord, Show)
82+
83+-- | Information about a mouse up event
84+data MouseUp = MouseUp
85+ { _mouseUp_button :: Maybe V.Button
86+ , _mouseUp_coordinates :: (Int, Int)
87+ }
88+ deriving (Eq, Ord, Show)
89+
90+-- | Mouse scroll direction
91+data ScrollDirection = ScrollDirection_Up | ScrollDirection_Down
92+ deriving (Eq, Ord, Show)
93+
94+-- | Produce an event that fires when the mouse wheel is scrolled
95+mouseScroll
96+ :: (Reflex t, Monad m, HasInput t m)
97+ => m (Event t ScrollDirection)
98+mouseScroll = do
99+ up <- mouseDown V.BScrollUp
100+ down <- mouseDown V.BScrollDown
101+ return $ leftmost
102+ [ ScrollDirection_Up <$ up
103+ , ScrollDirection_Down <$ down
104+ ]
105+
106+
src/Reflex/Vty/Widget/Input/Text.hs+74โˆ’27
@@ -2,15 +2,12 @@
2Module: Reflex.Vty.Widget.Input.Text
2Module: Reflex.Vty.Widget.Input.Text
3Description: Widgets for accepting text input from users and manipulating text within those inputs
3Description: Widgets for accepting text input from users and manipulating text within those inputs
4-}
4-}
5โˆ’{-# LANGUAGE FlexibleContexts #-}
6โˆ’{-# LANGUAGE LambdaCase #-}
7โˆ’{-# LANGUAGE OverloadedStrings #-}
8โˆ’{-# LANGUAGE RecursiveDo #-}
9module Reflex.Vty.Widget.Input.Text
5module Reflex.Vty.Widget.Input.Text
10 ( module Reflex.Vty.Widget.Input.Text
6 ( module Reflex.Vty.Widget.Input.Text
11 , def
7 , def
12 ) where
8 ) where
13
9
10+import Control.Monad (join)
14import Control.Monad.Fix (MonadFix)
11import Control.Monad.Fix (MonadFix)
15import Data.Default (Default(..))
12import Data.Default (Default(..))
16import Data.Text (Text)
13import Data.Text (Text)
@@ -19,54 +16,83 @@ import qualified Graphics.Vty as V
19import Reflex
16import Reflex
20
17
21import Reflex.Vty.Widget
18import Reflex.Vty.Widget
19+import Reflex.Vty.Widget.Layout
20+import Reflex.Vty.Widget.Input.Mouse
22
21
23-- | Configuration options for a 'textInput'. For more information on
22-- | Configuration options for a 'textInput'. For more information on
24-- 'TextZipper', see 'Data.Text.Zipper'.
23-- 'TextZipper', see 'Data.Text.Zipper'.
25data TextInputConfig t = TextInputConfig
24data TextInputConfig t = TextInputConfig
26 { _textInputConfig_initialValue :: TextZipper
25 { _textInputConfig_initialValue :: TextZipper
27 , _textInputConfig_modify :: Event t (TextZipper -> TextZipper)
26 , _textInputConfig_modify :: Event t (TextZipper -> TextZipper)
27+ , _textInputConfig_tabWidth :: Int
28+ , _textInputConfig_display :: Dynamic t (Char -> Char)
29+ -- ^ Transform the characters in a text input before displaying them. This is useful, e.g., for
30+ -- masking characters when entering passwords.
28 }
31 }
29
32
30instance Reflex t => Default (TextInputConfig t) where
33instance Reflex t => Default (TextInputConfig t) where
31โˆ’ def = TextInputConfig empty never
34+ def = TextInputConfig empty never 4 (pure id)
35+
36+-- | The output produced by text input widgets, including the text
37+-- value and the number of display lines (post-wrapping). Note that some
38+-- display lines may not be visible due to scrolling.
39+data TextInput t = TextInput
40+ { _textInput_value :: Dynamic t Text
41+ , _textInput_lines :: Dynamic t Int
42+ }
32
43
33-- | A widget that allows text input
44-- | A widget that allows text input
34textInput
45textInput
35โˆ’ :: (Reflex t, MonadHold t m, MonadFix m)
46+ :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m, HasFocusReader t m, HasTheme t m, HasDisplayRegion t m, HasImageWriter t m, HasDisplayRegion t m)
36 => TextInputConfig t
47 => TextInputConfig t
37โˆ’ -> VtyWidget t m (Dynamic t Text)
48+ -> m (TextInput t)
38textInput cfg = do
49textInput cfg = do
39 i <- input
50 i <- input
40 f <- focus
51 f <- focus
52+ dh <- displayHeight
53+ dw <- displayWidth
54+ bt <- theme
55+ attr0 <- sample bt
41 rec v <- foldDyn ($) (_textInputConfig_initialValue cfg) $ mergeWith (.)
56 rec v <- foldDyn ($) (_textInputConfig_initialValue cfg) $ mergeWith (.)
42โˆ’ [ updateTextZipper <$> i
57+ [ uncurry (updateTextZipper (_textInputConfig_tabWidth cfg)) <$> attach (current dh) i
43 , _textInputConfig_modify cfg
58 , _textInputConfig_modify cfg
44โˆ’ , let displayInfo = (,) <$> rows <*> scrollTop
59+ , let displayInfo = (,) <$> current rows <*> scrollTop
45โˆ’ in ffor (attach (current displayInfo) click) $ \((dl, st), MouseDown _ (mx, my) _) ->
60+ in ffor (attach displayInfo click) $ \((dl, st), MouseDown _ (mx, my) _) ->
46 goToDisplayLinePosition mx (st + my) dl
61 goToDisplayLinePosition mx (st + my) dl
47 ]
62 ]
48โˆ’ dw <- displayWidth
49โˆ’ dh <- displayHeight
50 click <- mouseDown V.BLeft
63 click <- mouseDown V.BLeft
51โˆ’ let cursorAttrs = ffor f $ \x -> if x then cursorAttributes else V.defAttr
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 = (,,)
68+ <$> dw
69+ <*> (mapZipper <$> _textInputConfig_display cfg <*> v)
70+ <*> f
71+ toDisplayLines attr (w, s, x) =
72+ let c = if x then toCursorAttrs attr else attr
52โˆ’ let rows = (\w s c -> displayLines w V.defAttr c s) <$> dw <*> v <*> cursorAttrs
73+ in displayLines w attr c s
74+ attrDyn <- holdDyn attr0 $ pushAlways (\_ -> sample bt) (updated rowInputDyn)
75+ let rows = ffor2 attrDyn rowInputDyn toDisplayLines
53 img = images . _displayLines_spans <$> rows
76 img = images . _displayLines_spans <$> rows
54โˆ’ y <- holdUniqDyn $ _displayLines_cursorY <$> rows
77+ y <- holdUniqDyn $ fmap snd _displayLines_cursorPos <$> rows
55 let newScrollTop :: Int -> (Int, Int) -> Int
78 let newScrollTop :: Int -> (Int, Int) -> Int
56 newScrollTop st (h, cursorY)
79 newScrollTop st (h, cursorY)
57 | cursorY < st = cursorY
80 | cursorY < st = cursorY
58 | cursorY >= st + h = cursorY - h + 1
81 | cursorY >= st + h = cursorY - h + 1
59 | otherwise = st
82 | otherwise = st
60โˆ’ rec let hy = attachWith newScrollTop (current scrollTop) $ updated $ zipDyn dh y
83+ let hy = attachWith newScrollTop scrollTop $ updated $ zipDyn dh y
61โˆ’ scrollTop <- holdDyn 0 hy
84+ scrollTop <- hold 0 hy
62โˆ’ tellImages $ (\imgs st -> (:[]) . V.vertCat $ drop st imgs) <$> current img <*> current scrollTop
85+ tellImages $ (\imgs st -> (:[]) . V.vertCat $ drop st imgs) <$> current img <*> scrollTop
63โˆ’ return $ value <$> v
86+ return $ TextInput
87+ { _textInput_value = value <$> v
88+ , _textInput_lines = length . _displayLines_spans <$> rows
89+ }
64
90
65-- | A widget that allows multiline text input
91-- | A widget that allows multiline text input
66multilineTextInput
92multilineTextInput
67โˆ’ :: (Reflex t, MonadHold t m, MonadFix m)
93+ :: (Reflex t, MonadHold t m, MonadFix m, HasInput t m, HasFocusReader t m, HasTheme t m, HasDisplayRegion t m, HasImageWriter t m)
68 => TextInputConfig t
94 => TextInputConfig t
69โˆ’ -> VtyWidget t m (Dynamic t Text)
95+ -> m (TextInput t)
70multilineTextInput cfg = do
96multilineTextInput cfg = do
71 i <- input
97 i <- input
72 textInput $ cfg
98 textInput $ cfg
@@ -78,9 +104,21 @@ multilineTextInput cfg = do
78 ]
104 ]
79 }
105 }
80
106
81โˆ’-- | Default attributes for the text cursor
107+-- | Wraps a 'textInput' or 'multilineTextInput' in a tile. Uses
82โˆ’cursorAttributes :: V.Attr
108+-- the computed line count to greedily size the tile when vertically
83โˆ’cursorAttributes = V.withStyle V.defAttr V.reverseVideo
109+-- oriented, and uses the fallback width when horizontally oriented.
110+textInputTile
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)
112+ => m (TextInput t)
113+ -> Dynamic t Int
114+ -> m (TextInput t)
115+textInputTile txt width = do
116+ o <- askOrientation
117+ rec t <- tile (Constraint_Fixed <$> sz) txt
118+ let sz = join $ ffor o $ \case
119+ Orientation_Column -> _textInput_lines t
120+ Orientation_Row -> width
121+ return t
84
122
85-- | Turn a set of display line rows into a list of images (one per line)
123-- | Turn a set of display line rows into a list of images (one per line)
86images :: [[Span V.Attr]] -> [V.Image]
124images :: [[Span V.Attr]] -> [V.Image]
@@ -90,13 +128,20 @@ images = map (V.horizCat . map spanToImage)
90image :: [[Span V.Attr]] -> V.Image
128image :: [[Span V.Attr]] -> V.Image
91image = V.vertCat . images
129image = V.vertCat . images
92
130
93โˆ’-- | Turn a 'Span' into an 'Image'
131+-- | Turn a 'Span' into an 'Graphics.Vty.Image'
94spanToImage :: Span V.Attr -> V.Image
132spanToImage :: Span V.Attr -> V.Image
95spanToImage (Span attrs t) = V.text' attrs t
133spanToImage (Span attrs t) = V.text' attrs t
96
134
97-- | Default vty event handler for text inputs
135-- | Default vty event handler for text inputs
98โˆ’updateTextZipper :: V.Event -> TextZipper -> TextZipper
136+updateTextZipper
137+ :: Int -- ^ Tab width
138+ -> Int -- ^ Page size
139+ -> V.Event -- ^ The vty event to handle
140+ -> TextZipper -- ^ The zipper to modify
141+ -> TextZipper
99โˆ’updateTextZipper ev = case ev of
142+updateTextZipper tabWidth pageSize ev = case ev of
143+ -- Special characters
144+ V.EvKey (V.KChar '\t') [] -> tab tabWidth
100 -- Regular characters
145 -- Regular characters
101 V.EvKey (V.KChar k) [] -> insertChar k
146 V.EvKey (V.KChar k) [] -> insertChar k
102 -- Deletion buttons
147 -- Deletion buttons
@@ -112,4 +157,6 @@ updateTextZipper ev = case ev of
112 V.EvKey V.KDown [] -> down
157 V.EvKey V.KDown [] -> down
113 V.EvKey V.KHome [] -> home
158 V.EvKey V.KHome [] -> home
114 V.EvKey V.KEnd [] -> end
159 V.EvKey V.KEnd [] -> end
160+ V.EvKey V.KPageUp [] -> pageUp pageSize
161+ V.EvKey V.KPageDown [] -> pageDown pageSize
115 _ -> id
162 _ -> id
src/Reflex/Vty/Widget/Layout.hsnew file+549
@@ -0,0 +1,549 @@
1+{-|
2+Module: Reflex.Vty.Widget.Layout
3+Description: Monad transformer and tools for arranging widgets and building screen layouts
4+-}
5+{-# Language UndecidableInstances #-}
6+
7+module Reflex.Vty.Widget.Layout where
8+
9+import Control.Applicative (liftA2)
10+import Control.Monad.Morph
11+import Control.Monad.NodeId (MonadNodeId(..), NodeId)
12+import Control.Monad.Reader
13+import Data.List (mapAccumL)
14+import Data.Map.Ordered (OMap)
15+import qualified Data.Map.Ordered as OMap
16+import Data.Maybe (fromMaybe, isNothing)
17+import Data.Ratio ((%))
18+import Data.Semigroup (First(..))
19+import Data.Set.Ordered (OSet)
20+import qualified Data.Set.Ordered as OSet
21+import qualified Graphics.Vty as V
22+
23+import Reflex
24+import Reflex.Host.Class (MonadReflexCreateTrigger)
25+import Reflex.Vty.Widget
26+import Reflex.Vty.Widget.Input.Mouse
27+
28+-- * Focus
29+--
30+-- $focus
31+--
32+-- The focus monad tracks which element is currently focused and processes
33+-- requests to change focus. Focusable elements are assigned a 'FocusId' and
34+-- can manually request focus or receive focus due to some other action (e.g.,
35+-- a tab press in a sibling element, a click event).
36+--
37+-- Focusable elements will usually be created via 'tile', but can also be
38+-- constructed via 'makeFocus' in 'HasFocus'. The latter option allows for
39+-- more find-grained control of focus behavior.
40+
41+-- ** Storing focus state
42+
43+-- | Identifies an element that is focusable. Can be created using 'makeFocus'.
44+newtype FocusId = FocusId NodeId
45+ deriving (Eq, Ord)
46+
47+-- | An ordered set of focus identifiers. The order here determines the order
48+-- in which focus cycles between focusable elements.
49+newtype FocusSet = FocusSet { unFocusSet :: OSet FocusId }
50+
51+instance Semigroup FocusSet where
52+ FocusSet a <> FocusSet b = FocusSet $ a OSet.|<> b
53+
54+instance Monoid FocusSet where
55+ mempty = FocusSet OSet.empty
56+
57+-- | Produces a 'FocusSet' with a single element
58+singletonFS :: FocusId -> FocusSet
59+singletonFS = FocusSet . OSet.singleton
60+
61+-- ** Changing focus state
62+
63+-- | Operations that change the currently focused element.
64+data Refocus = Refocus_Shift Int -- ^ Shift the focus by a certain number of positions (see 'shiftFS')
65+ | Refocus_Id FocusId -- ^ Focus a particular element
66+ | Refocus_Clear -- ^ Remove focus from all elements
67+
68+-- | Given a 'FocusSet', a currently focused element, and a number of positions
69+-- to move by, determine the newly focused element.
70+shiftFS :: FocusSet -> Maybe FocusId -> Int -> Maybe FocusId
71+shiftFS (FocusSet s) fid n = case OSet.findIndex <$> fid <*> pure s of
72+ Nothing -> OSet.elemAt s 0
73+ Just Nothing -> OSet.elemAt s 0
74+ Just (Just ix) -> OSet.elemAt s $ mod (ix + n) (OSet.size s)
75+
76+-- ** The focus management monad
77+
78+-- | A class for things that can produce focusable elements.
79+class (Monad m, Reflex t) => HasFocus t m | m -> t where
80+ -- | Create a focusable element.
81+ makeFocus :: m FocusId
82+ -- | Emit an 'Event' of requests to change the focus.
83+ requestFocus :: Event t Refocus -> m ()
84+ -- | Produce a 'Dynamic' that indicates whether the given 'FocusId' is focused.
85+ isFocused :: FocusId -> m (Dynamic t Bool)
86+ -- | Run an action, additionally returning the focusable elements it produced.
87+ subFoci :: m a -> m (a, Dynamic t FocusSet)
88+ -- | Get a 'Dynamic' of the currently focused element identifier.
89+ focusedId :: m (Dynamic t (Maybe FocusId))
90+
91+-- | A monad transformer that keeps track of the set of focusable elements and
92+-- which, if any, are currently focused, and allows focus requests.
93+newtype Focus t m a = Focus
94+ { unFocus :: DynamicWriterT t FocusSet
95+ (ReaderT (Dynamic t (Maybe FocusId))
96+ (EventWriterT t (First Refocus) m)) a
97+ }
98+ deriving
99+ ( Functor
100+ , Applicative
101+ , Monad
102+ , MonadHold t
103+ , MonadSample t
104+ , MonadFix
105+ , TriggerEvent t
106+ , PerformEvent t
107+ , NotReady t
108+ , MonadReflexCreateTrigger t
109+ , HasDisplayRegion t
110+ , PostBuild t
111+ , MonadNodeId
112+ , MonadIO
113+ )
114+
115+
116+instance MonadTrans (Focus t) where
117+ lift = Focus . lift . lift . lift
118+
119+instance MFunctor (Focus t) where
120+ hoist f = Focus . hoist (hoist (hoist f)) . unFocus
121+
122+instance (Adjustable t m, MonadHold t m, MonadFix m) => Adjustable t (Focus t m) where
123+ runWithReplace (Focus a) e = Focus $ runWithReplace a $ fmap unFocus e
124+ traverseIntMapWithKeyWithAdjust f m e = Focus $ traverseIntMapWithKeyWithAdjust (\k v -> unFocus $ f k v) m e
125+ traverseDMapWithKeyWithAdjust f m e = Focus $ traverseDMapWithKeyWithAdjust (\k v -> unFocus $ f k v) m e
126+ traverseDMapWithKeyWithAdjustWithMove f m e = Focus $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unFocus $ f k v) m e
127+
128+instance (Reflex t, MonadFix m, HasInput t m) => HasInput t (Focus t m) where
129+ localInput f = hoist (localInput f)
130+
131+instance (HasImageWriter t m, MonadFix m) => HasImageWriter t (Focus t m) where
132+ mapImages f = hoist (mapImages f)
133+
134+instance (HasFocusReader t m, Monad m) => HasFocusReader t (Focus t m)
135+
136+instance (HasTheme t m, Monad m) => HasTheme t (Focus t m)
137+
138+instance (Reflex t, MonadFix m, MonadNodeId m) => HasFocus t (Focus t m) where
139+ makeFocus = do
140+ fid <- FocusId <$> lift getNextNodeId
141+ Focus $ tellDyn $ pure $ singletonFS fid
142+ pure fid
143+ requestFocus = Focus . tellEvent . fmap First
144+ isFocused fid = do
145+ sel <- Focus ask
146+ pure $ (== Just fid) <$> sel
147+ subFoci (Focus child) = Focus $ do
148+ (a, fs) <- lift $ runDynamicWriterT child
149+ tellDyn fs
150+ return (a, fs)
151+ focusedId = Focus ask
152+
153+-- | Runs a 'Focus' action, maintaining the selection state internally.
154+runFocus
155+ :: (MonadFix m, MonadHold t m, Reflex t)
156+ => Focus t m a
157+ -> m (a, Dynamic t FocusSet)
158+runFocus (Focus x) = do
159+ rec ((a, focusIds), focusRequests) <- runEventWriterT $ flip runReaderT sel $ runDynamicWriterT x
160+ sel <- foldDyn f Nothing $ attach (current focusIds) focusRequests
161+ pure (a, focusIds)
162+ where
163+ f :: (FocusSet, First Refocus) -> Maybe FocusId -> Maybe FocusId
164+ f (fs, rf) mf = case getFirst rf of
165+ Refocus_Clear -> Nothing
166+ Refocus_Id fid -> Just fid
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)
176+ -> m a
177+anyChildFocused f = do
178+ fid <- focusedId
179+ rec (a, fs) <- subFoci (f b)
180+ let b = liftA2 (\foc s -> case foc of
181+ Nothing -> False
182+ Just f' -> OSet.member f' $ unFocusSet s) fid fs
183+ pure a
184+
185+-- ** Focus controls
186+
187+-- | Request focus be shifted backward and forward based on tab presses. <Tab>
188+-- shifts focus forward and <Shift+Tab> shifts focus backward.
189+tabNavigation :: (Reflex t, HasInput t m, HasFocus t m) => m ()
190+tabNavigation = do
191+ fwd <- fmap (const 1) <$> key (V.KChar '\t')
192+ back <- fmap (const (-1)) <$> key V.KBackTab
193+ requestFocus $ Refocus_Shift <$> leftmost [fwd, back]
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
235+ => 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
249+ -> 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
256+ -> m a
257+col = axis (pure Orientation_Column) flex
258+
259+-- ** Layout management data
260+
261+-- | A collection of information related to the layout of the screen. The root
262+-- node is a "parent" widget, and the contents of the 'LayoutForest' are its
263+-- children.
264+data LayoutTree a = LayoutTree a (LayoutForest a)
265+ deriving (Show)
266+
267+-- | An ordered, indexed collection of 'LayoutTree's representing information
268+-- about the children of some widget.
269+newtype LayoutForest a = LayoutForest { unLayoutForest :: OMap NodeId (LayoutTree a) }
270+ deriving (Show)
271+
272+instance Semigroup (LayoutForest a) where
273+ LayoutForest a <> LayoutForest b = LayoutForest $ a OMap.|<> b
274+
275+instance Monoid (LayoutForest a) where
276+ mempty = LayoutForest OMap.empty
277+
278+-- | Perform a lookup by 'NodeId' in a 'LayoutForest'
279+lookupLF :: NodeId -> LayoutForest a -> Maybe (LayoutTree a)
280+lookupLF n (LayoutForest a) = OMap.lookup n a
281+
282+-- | Create a 'LayoutForest' with one element
283+singletonLF :: NodeId -> LayoutTree a -> LayoutForest a
284+singletonLF n t = LayoutForest $ OMap.singleton (n, t)
285+
286+-- | Produce a 'LayoutForest' from a list. The order of the list is preserved.
287+fromListLF :: [(NodeId, LayoutTree a)] -> LayoutForest a
288+fromListLF = LayoutForest . OMap.fromList
289+
290+-- | Extract the information at the root of a 'LayoutTree'
291+rootLT :: LayoutTree a -> a
292+rootLT (LayoutTree a _) = a
293+
294+-- | Extract the child nodes of a 'LayoutTree'
295+childrenLT :: LayoutTree a -> LayoutForest a
296+childrenLT (LayoutTree _ a) = a
297+
298+-- | Produce a layout solution given a starting orientation, the overall screen
299+-- size, and a set of constraints.
300+solve
301+ :: Orientation
302+ -> Region
303+ -> LayoutForest (Constraint, Orientation)
304+ -> LayoutTree (Region, Orientation)
305+solve o0 r0 (LayoutForest cs) =
306+ let a = map (\(x, t@(LayoutTree (c, _) _)) -> ((x, t), c)) $ OMap.assocs cs
307+ extent = case o0 of
308+ Orientation_Row -> _region_width r0
309+ Orientation_Column -> _region_height r0
310+ sizes = computeEdges $ computeSizes extent a
311+ chunks = [ (nodeId, solve o1 r1 f)
312+ | ((nodeId, LayoutTree (_, o1) f), sz) <- sizes
313+ , let r1 = chunk o0 r0 sz
314+ ]
315+ in LayoutTree (r0, o0) $ fromListLF chunks
316+ where
317+ computeEdges :: [(a, Int)] -> [(a, (Int, Int))]
318+ computeEdges = ($ []) . fst . foldl (\(m, offset) (a, sz) ->
319+ (((a, (offset, sz)) :) . m, sz + offset)) (id, 0)
320+ computeSizes
321+ :: Int
322+ -> [(a, Constraint)]
323+ -> [(a, Int)]
324+ computeSizes available constraints =
325+ -- The minimum amount of space we need. Calculated by adding up all of
326+ -- the fixed size items and all the minimum sizes of stretchable items
327+ let minTotal = sum $ ffor constraints $ \case
328+ (_, Constraint_Fixed n) -> n
329+ (_, Constraint_Min n) -> n
330+ -- The leftover space is the area we can allow stretchable items to
331+ -- expand into
332+ leftover = max 0 (available - minTotal)
333+ -- The number of stretchable items that will try to share some of the
334+ -- leftover space
335+ numStretch = length $ filter (isMin . snd) constraints
336+ -- Space to allocate to the stretchable items (this is the same for all
337+ -- items and there may still be additional leftover space that will have
338+ -- to be unevenly distributed)
339+ szStretch = floor $ leftover % max numStretch 1
340+ -- Remainder of available space after even distribution. This extra space
341+ -- will be distributed to as many stretchable widgets as possible.
342+ adjustment = max 0 $ available - minTotal - szStretch * numStretch
343+ in snd $ mapAccumL (\adj (a, c) -> case c of
344+ Constraint_Fixed n -> (adj, (a, n))
345+ Constraint_Min n -> (max 0 (adj-1), (a, n + szStretch + signum adj))) adjustment constraints
346+ isMin (Constraint_Min _) = True
347+ isMin _ = False
348+
349+-- | Produce a 'Region' given a starting orientation and region, and the offset
350+-- and main-axis size of the chunk.
351+chunk :: Orientation -> Region -> (Int, Int) -> Region
352+chunk o r (offset, sz) = case o of
353+ Orientation_Column -> r
354+ { _region_top = _region_top r + offset
355+ , _region_height = sz
356+ }
357+ Orientation_Row -> r
358+ { _region_left = _region_left r + offset
359+ , _region_width = sz
360+ }
361+
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+
462+-- | Runs a 'Layout' action, using the given orientation and region to
463+-- calculate layout solutions.
464+runLayout
465+ :: (MonadFix m, Reflex t)
466+ => Dynamic t Orientation
467+ -> Dynamic t Region
468+ -> Layout t m a
469+ -> m a
470+runLayout o r (Layout x) = do
471+ rec (result, w) <- runReaderT (runDynamicWriterT x) solutions
472+ let solutions = solve <$> o <*> r <*> w
473+ return result
474+
475+-- | Initialize and run the layout monad, using all of the available screen space.
476+initLayout :: (HasDisplayRegion t m, MonadFix m) => Layout t m a -> m a
477+initLayout f = do
478+ dw <- displayWidth
479+ dh <- displayHeight
480+ let r = Region 0 0 <$> dw <*> dh
481+ runLayout (pure Orientation_Column) r f
482+
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.
489+
490+-- | Initialize a 'Layout' and 'Focus' management context, returning the produced 'FocusSet'.
491+initManager
492+ :: (HasDisplayRegion t m, Reflex t, MonadHold t m, MonadFix m)
493+ => Layout t (Focus t m) a
494+ -> m (a, Dynamic t FocusSet)
495+initManager =
496+ runFocus . initLayout
497+
498+-- | Initialize a 'Layout' and 'Focus' management context.
499+initManager_
500+ :: (HasDisplayRegion t m, Reflex t, MonadHold t m, MonadFix m)
501+ => Layout t (Focus t m) a
502+ -> m a
503+initManager_ = 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