mirror of
https://github.com/jtdaugherty/brick.git
synced 2024-12-28 00:12:02 +03:00
Merge branch 'documentation/brick-1.0-staging'
This commit is contained in:
commit
2b467f55e3
79
CHANGELOG.md
79
CHANGELOG.md
@ -2,6 +2,85 @@
|
|||||||
Brick changelog
|
Brick changelog
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
1.0
|
||||||
|
---
|
||||||
|
|
||||||
|
Version 1.0 of `brick` comes with some improvements that will require
|
||||||
|
you to update your programs. This section details the list of API
|
||||||
|
changes in 1.0 that are likely to introduce breakage and how to deal
|
||||||
|
with each one. You can also consult the demonstration
|
||||||
|
programs to see orking examples of the new API. For those
|
||||||
|
interested in a bit of discussion on the changes, see [this
|
||||||
|
ticket](https://github.com/jtdaugherty/brick/issues/379).
|
||||||
|
|
||||||
|
* The event-handling monad `EventM` was improved and changed in some
|
||||||
|
substantial ways, all aimed at making `EventM` code cleaner, more
|
||||||
|
composable, and more amenable to lens updates to the application
|
||||||
|
state.
|
||||||
|
* The type has changed from `EventM n a` to `EventM n s a` and is now
|
||||||
|
an `mtl`-compatible state monad over `s`. Some consequences and
|
||||||
|
related changes are:
|
||||||
|
* Event handlers no longer take and return an explicit state value;
|
||||||
|
an event handler that formerly had the type `handler :: s ->
|
||||||
|
BrickEvent n e -> EventM n (Next s)` now has type `handler ::
|
||||||
|
BrickEvent n e -> EventM n s ()`. This also affected all of
|
||||||
|
Brick's built-in event handler functions for `List`, `Editor`,
|
||||||
|
etc.
|
||||||
|
* The `appHandleEvent` and `appStartEvent` fields of `App` changed
|
||||||
|
types to reflect the new structure of `EventM`. `appStartEvent`
|
||||||
|
will just be `return ()` rather than `return` for most
|
||||||
|
applications.
|
||||||
|
* `EventM` can be used with the `MonadState` API from `mtl` as well
|
||||||
|
as with the very nice lens combinators in `microlens-mtl`.
|
||||||
|
* The `Next` type was removed.
|
||||||
|
* State-specific event handlers like `handleListEvent` and
|
||||||
|
`handleEditorEvent` are now statically typed to be scoped to
|
||||||
|
just the states they manage, so `zoom` from `microlens-mtl` must
|
||||||
|
be used to invoke them. `Brick.Types` re-exports `zoom` for
|
||||||
|
convenience. `handleEventLensed` was removed from the API in lieu
|
||||||
|
of the new `zoom` behavior. Code that previously handled events
|
||||||
|
with `handleEventLensed s someLens someHandler e` is now just
|
||||||
|
written `zoom someLens $ someHandler e`.
|
||||||
|
* If an `EventM` block needs to operate on some state `s` that is
|
||||||
|
not accessible via a lens into the application state, the `EventM`
|
||||||
|
block can be set up with `Brick.Types.nestEventM`.
|
||||||
|
* Since `Next` was removed, control flow is now as follows:
|
||||||
|
* Without any explicit specification, an `EventM` block always
|
||||||
|
continues execution of the `brick` event loop when it finishes.
|
||||||
|
`continue` was removed from the API. What was previously `continue
|
||||||
|
$ s & someLens .~ value` will become `someLens .= value`.
|
||||||
|
* `halt` is still used to indicate that the event loop should halt
|
||||||
|
after the calling handler is finished, but `halt` no longer takes
|
||||||
|
an explicit state value argument.
|
||||||
|
* `suspendAndResume` is now immediate; previously,
|
||||||
|
`suspendAndResume` indicated that the specified action should run
|
||||||
|
once the event handler finished. Now, the event handler is paused
|
||||||
|
while the specified action is run. This allows `EventM` code to
|
||||||
|
continue to run after `suspendAndResume` is called and before
|
||||||
|
control is returned to `brick`.
|
||||||
|
* Brick now depends on `mtl` rather than `transformers`.
|
||||||
|
* The `IsString` instance for `AttrName` was removed.
|
||||||
|
* This change is motivated by the API wart that resulted from the
|
||||||
|
overloading of both `<>` and string literals (via
|
||||||
|
`OverloadedStrings`) that resulted in code such as `someAttrName
|
||||||
|
= "blah" <> "things"`. While that worked to create an `AttrName`
|
||||||
|
with two segments, it was far too easy to read as two strings
|
||||||
|
concatenated. The overloading hid what is really going on with the
|
||||||
|
segments of the attribute name. The way to write the above example
|
||||||
|
after this change is `someAttrName = attrName "blah" <> attrName
|
||||||
|
"things"`.
|
||||||
|
|
||||||
|
Other changes in this release:
|
||||||
|
|
||||||
|
* Brick now provides an optional API for user-defined keybindings
|
||||||
|
for applications! See the User Guide section "Customizable
|
||||||
|
Keybindings", the Haddock for `Brick.Keybindings.KeyDispatcher`,
|
||||||
|
and the new demo program `programs/CustomKeybindingDemo.hs` to get
|
||||||
|
started.
|
||||||
|
* `Brick.Widgets.List` got `listSelectedElementL`, a traversal for
|
||||||
|
accessing the currently selected element of a list. (Thanks Fraser
|
||||||
|
Tweedale)
|
||||||
|
|
||||||
0.73
|
0.73
|
||||||
----
|
----
|
||||||
|
|
||||||
|
@ -119,7 +119,6 @@ Documentation
|
|||||||
Documentation for `brick` comes in a variety of forms:
|
Documentation for `brick` comes in a variety of forms:
|
||||||
|
|
||||||
* [The official brick user guide](https://github.com/jtdaugherty/brick/blob/master/docs/guide.rst)
|
* [The official brick user guide](https://github.com/jtdaugherty/brick/blob/master/docs/guide.rst)
|
||||||
* [Samuel Tay's brick tutorial](https://github.com/jtdaugherty/brick/blob/master/docs/samtay-tutorial.md)
|
|
||||||
* Haddock (all modules)
|
* Haddock (all modules)
|
||||||
* [Demo programs](https://github.com/jtdaugherty/brick/blob/master/programs) ([Screenshots](https://github.com/jtdaugherty/brick/blob/master/docs/programs-screenshots.md))
|
* [Demo programs](https://github.com/jtdaugherty/brick/blob/master/programs) ([Screenshots](https://github.com/jtdaugherty/brick/blob/master/docs/programs-screenshots.md))
|
||||||
* [FAQ](https://github.com/jtdaugherty/brick/blob/master/FAQ.md)
|
* [FAQ](https://github.com/jtdaugherty/brick/blob/master/FAQ.md)
|
||||||
|
@ -42,7 +42,6 @@ tested-with: GHC == 8.2.2, GHC == 8.4.4, GHC == 8.6.5, GHC == 8.8.4, GHC
|
|||||||
|
|
||||||
extra-doc-files: README.md,
|
extra-doc-files: README.md,
|
||||||
docs/guide.rst,
|
docs/guide.rst,
|
||||||
docs/samtay-tutorial.md,
|
|
||||||
docs/snake-demo.gif,
|
docs/snake-demo.gif,
|
||||||
CHANGELOG.md,
|
CHANGELOG.md,
|
||||||
programs/custom_keys.ini,
|
programs/custom_keys.ini,
|
||||||
|
560
docs/guide.rst
560
docs/guide.rst
@ -68,16 +68,17 @@ Conventions
|
|||||||
documentation and as you explore the library source and write your own
|
documentation and as you explore the library source and write your own
|
||||||
programs.
|
programs.
|
||||||
|
|
||||||
- Use of `microlens`_ packages: ``brick`` uses ``microlens`` family of
|
- Use of `microlens`_ packages: ``brick`` uses the ``microlens`` family
|
||||||
packages internally and also exposes lenses for many types in the
|
of packages internally and also exposes lenses for many types in the
|
||||||
library. However, if you prefer not to use the lens interface in your
|
library. However, if you prefer not to use the lens interface in your
|
||||||
program, all lens interfaces have non-lens equivalents exported by
|
program, all lens interfaces have non-lens equivalents exported by
|
||||||
the same module. In general, the "``L``" suffix on something tells
|
the same module. In general, the "``L``" suffix on something tells
|
||||||
you it is a lens; the name without the "``L``" suffix is the non-lens
|
you it is a lens; the name without the "``L``" suffix is the non-lens
|
||||||
version. You can get by without using ``brick``'s lens interface but
|
version. You can get by without using ``brick``'s lens interface
|
||||||
your life will probably be much more pleasant once your application
|
but your life will probably be much more pleasant if you use lenses
|
||||||
state becomes sufficiently complex if you use lenses to modify it (see
|
to modify your application state once it state becomes sufficiently
|
||||||
`appHandleEvent: Handling Events`_).
|
complex (see `appHandleEvent: Handling Events`_ and `Event Handlers
|
||||||
|
for Component State`_).
|
||||||
- Attribute names: some modules export attribute names (see `How
|
- Attribute names: some modules export attribute names (see `How
|
||||||
Attributes Work`_) associated with user interface elements. These tend
|
Attributes Work`_) associated with user interface elements. These tend
|
||||||
to end in an "``Attr``" suffix (e.g. ``borderAttr``). In addition,
|
to end in an "``Attr``" suffix (e.g. ``borderAttr``). In addition,
|
||||||
@ -96,8 +97,8 @@ Compiling Brick Applications
|
|||||||
Brick applications must be compiled with the threaded RTS using the GHC
|
Brick applications must be compiled with the threaded RTS using the GHC
|
||||||
``-threaded`` option.
|
``-threaded`` option.
|
||||||
|
|
||||||
The App Type
|
The ``App`` Type
|
||||||
============
|
================
|
||||||
|
|
||||||
To use the library we must provide it with a value of type
|
To use the library we must provide it with a value of type
|
||||||
``Brick.Main.App``. This type is a record type whose fields perform
|
``Brick.Main.App``. This type is a record type whose fields perform
|
||||||
@ -108,8 +109,8 @@ various functions:
|
|||||||
data App s e n =
|
data App s e n =
|
||||||
App { appDraw :: s -> [Widget n]
|
App { appDraw :: s -> [Widget n]
|
||||||
, appChooseCursor :: s -> [CursorLocation n] -> Maybe (CursorLocation n)
|
, appChooseCursor :: s -> [CursorLocation n] -> Maybe (CursorLocation n)
|
||||||
, appHandleEvent :: s -> BrickEvent n e -> EventM n (Next s)
|
, appHandleEvent :: BrickEvent n e -> EventM n s ()
|
||||||
, appStartEvent :: s -> EventM n s
|
, appStartEvent :: EventM n s ()
|
||||||
, appAttrMap :: s -> AttrMap
|
, appAttrMap :: s -> AttrMap
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,16 +151,16 @@ To run an ``App``, we pass it to ``Brick.Main.defaultMain`` or
|
|||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
let app = App { ... }
|
let app = App { ... }
|
||||||
initialState = ...
|
initialState = ...
|
||||||
finalState <- defaultMain app initialState
|
finalState <- defaultMain app initialState
|
||||||
-- Use finalState and exit
|
-- Use finalState and exit
|
||||||
|
|
||||||
The ``customMain`` function is for more advanced uses; for details see
|
The ``customMain`` function is for more advanced uses; for details see
|
||||||
`Using Your Own Event Type`_.
|
`Using Your Own Event Type`_.
|
||||||
|
|
||||||
appDraw: Drawing an Interface
|
``appDraw``: Drawing an Interface
|
||||||
-----------------------------
|
---------------------------------
|
||||||
|
|
||||||
The value of ``appDraw`` is a function that turns the current
|
The value of ``appDraw`` is a function that turns the current
|
||||||
application state into a list of *layers* of type ``Widget``, listed
|
application state into a list of *layers* of type ``Widget``, listed
|
||||||
@ -201,115 +202,161 @@ The most important module providing drawing functions is
|
|||||||
``Brick.Widgets.Core``. Beyond that, any module in the ``Brick.Widgets``
|
``Brick.Widgets.Core``. Beyond that, any module in the ``Brick.Widgets``
|
||||||
namespace provides specific kinds of functionality.
|
namespace provides specific kinds of functionality.
|
||||||
|
|
||||||
appHandleEvent: Handling Events
|
``appHandleEvent``: Handling Events
|
||||||
-------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
The value of ``appHandleEvent`` is a function that decides how to modify
|
The value of ``appHandleEvent`` is a function that decides how to modify
|
||||||
the application state as a result of an event:
|
the application state as a result of an event:
|
||||||
|
|
||||||
.. code:: haskell
|
.. code:: haskell
|
||||||
|
|
||||||
appHandleEvent :: s -> BrickEvent n e -> EventM n (Next s)
|
appHandleEvent :: BrickEvent n e -> EventM n s ()
|
||||||
|
|
||||||
The first parameter of type ``s`` is your application's state at the
|
``appHandleEvent`` is responsible for deciding how to change the state
|
||||||
time the event arrives. ``appHandleEvent`` is responsible for deciding
|
based on the event. The single parameter to the event handler is the
|
||||||
how to change the state based on the event and then return it.
|
event to be handled. Its type variables ``n`` and ``e`` correspond
|
||||||
|
to the *resource name type* and *event type* of your application,
|
||||||
|
respectively, and must match the corresponding types in ``App`` and
|
||||||
|
``EventM``.
|
||||||
|
|
||||||
The second parameter of type ``BrickEvent n e`` is the event itself.
|
The ``EventM`` monad is parameterized on the *resource name type*
|
||||||
The type variables ``n`` and ``e`` correspond to the *resource name
|
``n`` and your application's state type ``s``. The ``EventM`` monad
|
||||||
type* and *event type* of your application, respectively, and must match
|
is a state monad over ``s``, so one way to access and modify your
|
||||||
the corresponding types in ``App`` and ``EventM``.
|
application's state in an event handler is to use the ``MonadState``
|
||||||
|
type class and associated operations from the ``mtl`` package. The
|
||||||
|
recommended approach, however, is to use the lens operations from the
|
||||||
|
``microlens-mtl`` package with lenses to perform concise state updates.
|
||||||
|
We'll cover this topic in more detail in `Event Handlers for Component
|
||||||
|
State`_.
|
||||||
|
|
||||||
The return value type ``Next s`` value describes what should happen
|
Once the event handler has performed any relevant state updates, it can
|
||||||
after the event handler is finished. We have four choices:
|
also indicate what should happen once the event handler has finished
|
||||||
|
executing. By default, after an event handler has completed, Brick will
|
||||||
|
redraw the screen with the application state (by calling ``appDraw``)
|
||||||
|
and wait for the next input event. However, there are two other options:
|
||||||
|
|
||||||
* ``Brick.Main.continue s``: continue executing the event loop with the
|
* ``Brick.Main.halt``: halt the event loop. The application state as it
|
||||||
specified application state ``s`` as the next value. Commonly this is
|
exists after the event handler completes is returned to the caller
|
||||||
where you'd modify the state based on the event and return it.
|
of ``defaultMain`` or ``customMain``.
|
||||||
* ``Brick.Main.continueWithoutRedraw s``: continue executing the event
|
* ``Brick.Main.continueWithoutRedraw``: continue executing the event
|
||||||
loop with the specified application state ``s`` as the next value, but
|
loop, but do not redraw the screen using the new state before waiting
|
||||||
unlike ``continue``, do not redraw the screen using the new state.
|
for another input event. This is faster than the default continue
|
||||||
This is a faster version of ``continue`` since it doesn't redraw the
|
behavior since it doesn't redraw the screen; it just leaves up the
|
||||||
screen; it just leaves up the previous screen contents. This function
|
previous screen contents. This function is only useful when you know
|
||||||
is only useful when you know that your state change won't cause
|
that your event handler's state change(s) won't cause anything on
|
||||||
anything on the screen to change. When in doubt, use ``continue``.
|
the screen to change. Use this only when you are certain that no
|
||||||
* ``Brick.Main.halt s``: halt the event loop and return the final
|
redraw of the screen is needed *and* when you are trying to address a
|
||||||
application state value ``s``. This state value is returned to the
|
performance problem. (See also `The Rendering Cache`_ for details on
|
||||||
caller of ``defaultMain`` or ``customMain`` where it can be used prior
|
how to detail with rendering performance issues.)
|
||||||
to finally exiting ``main``.
|
|
||||||
* ``Brick.Main.suspendAndResume act``: suspend the ``brick`` event loop
|
|
||||||
and execute the specified ``IO`` action ``act``. The action ``act``
|
|
||||||
must be of type ``IO s``, so when it executes it must return the next
|
|
||||||
application state. When ``suspendAndResume`` is used, the ``brick``
|
|
||||||
event loop is shut down and the terminal state is restored to its
|
|
||||||
state when the ``brick`` event loop began execution. When it finishes
|
|
||||||
executing, the event loop will be resumed using the returned state
|
|
||||||
value. This is useful for situations where your program needs to
|
|
||||||
suspend your interface and execute some other program that needs to
|
|
||||||
gain control of the terminal (such as an external editor).
|
|
||||||
|
|
||||||
The ``EventM`` monad is the event-handling monad. This monad is a
|
The ``EventM`` monad is a transformer around ``IO`` so I/O is possible
|
||||||
transformer around ``IO`` so you are free to do I/O in this monad by
|
in this monad by using ``liftIO``. Keep in mind, however, that event
|
||||||
using ``liftIO``. Beyond I/O, this monad is used to make scrolling
|
handlers should execute as quickly as possible to avoid introducing
|
||||||
requests to the renderer (see `Viewports`_) and obtain named extents
|
screen redraw latency. Consider using background threads to work
|
||||||
(see `Extents`_). Keep in mind that time spent blocking in your event
|
asynchronously when it would otherwise cause redraw latency.
|
||||||
handler is time during which your UI is unresponsive, so consider this
|
|
||||||
when deciding whether to have background threads do work instead of
|
|
||||||
inlining the work in the event handler.
|
|
||||||
|
|
||||||
Widget Event Handlers
|
Beyond I/O, ``EventM`` is used to make scrolling requests to the
|
||||||
*********************
|
renderer (see `Viewports`_), obtain named extents (see `Extents`_), and
|
||||||
|
other duties.
|
||||||
|
|
||||||
Event handlers are responsible for transforming the application state.
|
Event Handlers for Component State
|
||||||
While you can use ordinary methods to do this such as pattern matching
|
**********************************
|
||||||
and pure function calls, some widget state types such as the ones
|
|
||||||
provided by the ``Brick.Widgets.List`` and ``Brick.Widgets.Edit``
|
|
||||||
modules provide their own widget-specific event-handling functions.
|
|
||||||
For example, ``Brick.Widgets.Edit`` provides ``handleEditorEvent`` and
|
|
||||||
``Brick.Widgets.List`` provides ``handleListEvent``.
|
|
||||||
|
|
||||||
Since these event handlers run in ``EventM``, they have access to
|
The top-level ``appHandleEvent`` handler is responsible for managing
|
||||||
rendering viewport states via ``Brick.Main.lookupViewport`` and the
|
the application state, but it also needs to be able to update the state
|
||||||
``IO`` monad via ``liftIO``.
|
associated with UI components such as those that come with Brick.
|
||||||
|
|
||||||
To use these handlers in your program, invoke them on the relevant piece
|
For example, consider an application that uses Brick's built-in text
|
||||||
of state in your application state. In the following example we use an
|
editor from ``Brick.Widgets.Edit``. The built-in editor is similar to
|
||||||
``Edit`` state from ``Brick.Widgets.Edit``:
|
the main application in that it has three important elements:
|
||||||
|
|
||||||
|
* The editor state of type ``Editor t n``: this stores the editor's
|
||||||
|
contents, cursor position, etc.
|
||||||
|
* The editor's drawing function, ``renderEditor``: this is responsible
|
||||||
|
for drawing the editor in the UI.
|
||||||
|
* The editor's event handler, ``handleEditorEvent``: this is responsible
|
||||||
|
for updating the editor's contents and cursor position in response to
|
||||||
|
key events.
|
||||||
|
|
||||||
|
To use the built-in editor, the application must:
|
||||||
|
|
||||||
|
* Embed an ``Editor t n`` somewhere in the application state ``s``,
|
||||||
|
* Render the editor's state at the appropriate place in ``appDraw`` with
|
||||||
|
``renderEditor``, and
|
||||||
|
* Dispatch events to the editor in the ``appHandleEvent`` with
|
||||||
|
``handleEditorEvent``.
|
||||||
|
|
||||||
|
An example application state using an editor might look like this:
|
||||||
|
|
||||||
.. code:: haskell
|
.. code:: haskell
|
||||||
|
|
||||||
data Name = Edit1
|
data MyState = MyState { _editor :: Editor Text n }
|
||||||
type MyState = Editor String Name
|
|
||||||
|
|
||||||
myEvent :: MyState -> BrickEvent n e -> EventM Name (Next MyState)
|
|
||||||
myEvent s (VtyEvent e) = continue =<< handleEditorEvent e s
|
|
||||||
|
|
||||||
This pattern works well enough when your application state has an
|
|
||||||
event handler as shown in the ``Edit`` example above, but it can
|
|
||||||
become unpleasant if the value on which you want to invoke a handler
|
|
||||||
is embedded deeply within your application state. If you have chosen
|
|
||||||
to generate lenses for your application state fields, you can use the
|
|
||||||
convenience function ``handleEventLensed`` by specifying your state, a
|
|
||||||
lens, and the event:
|
|
||||||
|
|
||||||
.. code:: haskell
|
|
||||||
|
|
||||||
data Name = Edit1
|
|
||||||
data MyState = MyState { _theEdit :: Editor String Name
|
|
||||||
}
|
|
||||||
makeLenses ''MyState
|
makeLenses ''MyState
|
||||||
|
|
||||||
myEvent :: MyState -> BrickEvent n e -> EventM Name (Next MyState)
|
This declares the ``MyState`` type with an ``Editor`` contained within
|
||||||
myEvent s (VtyEvent e) = continue =<< handleEventLensed s theEdit handleEditorEvent e
|
it and uses Template Haskell to generate a lens, ``editor``, to allow us
|
||||||
|
to easily update the editor state in our event handler.
|
||||||
|
|
||||||
You might consider that preferable to the desugared version:
|
To dispatch events to the ``editor`` we'd start by writing the
|
||||||
|
application event handler:
|
||||||
|
|
||||||
.. code:: haskell
|
.. code:: haskell
|
||||||
|
|
||||||
myEvent :: MyState -> BrickEvent n e -> EventM Name (Next MyState)
|
handleEvent :: BrickEvent n e -> EventM n MyState ()
|
||||||
myEvent s (VtyEvent e) = do
|
handleEvent e = do
|
||||||
newVal <- handleEditorEvent e (s^.theEdit)
|
...
|
||||||
continue $ s & theEdit .~ newVal
|
|
||||||
|
But there's a problem: ``handleEditorEvent``'s type indicates that it
|
||||||
|
can only run over a state of type ``Editor t n``, but our handler runs
|
||||||
|
on ``MyState``. Specifically, ``handleEditorEvent`` has this type:
|
||||||
|
|
||||||
|
.. code:: haskell
|
||||||
|
|
||||||
|
handleEditorEvent :: BrickEvent n e -> EventM n (Editor t n) ()
|
||||||
|
|
||||||
|
This means that to use ``handleEditorEvent``, it must be composed
|
||||||
|
into the application's event handler, but since the state types ``s``
|
||||||
|
and ``Editor t n`` do not match, we need a way to compose these event
|
||||||
|
handlers. There are two ways to do this:
|
||||||
|
|
||||||
|
* Use ``Lens.Micro.Mtl.zoom`` from the ``microlens-mtl`` package
|
||||||
|
(re-exported by ``Brick.Types`` for convenience). This function is
|
||||||
|
required when you want to change the state type to a field embedded in
|
||||||
|
your application state using a lens. For example:
|
||||||
|
|
||||||
|
.. code:: haskell
|
||||||
|
|
||||||
|
handleEvent :: BrickEvent n e -> EventM n MyState ()
|
||||||
|
handleEvent e = do
|
||||||
|
zoom editor $ handleEditorEvent e
|
||||||
|
|
||||||
|
* Use ``Brick.Types.nestEventM``: this function lets you provide a state
|
||||||
|
value and run ``EventM`` using that state. The following
|
||||||
|
``nestEventM`` example is equivalent to the ``zoom`` example above:
|
||||||
|
|
||||||
|
.. code:: haskell
|
||||||
|
|
||||||
|
import Lens.Micro (_1)
|
||||||
|
import Lens.Micro.Mtl (use, (.=))
|
||||||
|
|
||||||
|
handleEvent :: BrickEvent n e -> EventM n MyState ()
|
||||||
|
handleEvent e = do
|
||||||
|
editorState <- use editor
|
||||||
|
(newEditorState, ()) <- nestEventM editorState $ do
|
||||||
|
handleEditorEvent e
|
||||||
|
editor .= newEditorState
|
||||||
|
|
||||||
|
The ``zoom`` function, together with lenses for your application state's
|
||||||
|
fields, is by far the best way to manage your state in ``EventM``. As
|
||||||
|
you can see from the examples above, the ``zoom`` approach avoids a lot
|
||||||
|
of boilerplate. The ``nestEventM`` approach is provided in cases where
|
||||||
|
the state that you need to mutate is not easily accessed by ``zoom``.
|
||||||
|
|
||||||
|
Finally, if you prefer to avoid the use of lenses, you can always use
|
||||||
|
the ``MonadState`` API to get, put, and modify your state. Keep in
|
||||||
|
mind that the ``MonadState`` approach will still require the use of
|
||||||
|
``nestEventM`` when events scoped to widget states such as ``Editor``
|
||||||
|
need to be handled.
|
||||||
|
|
||||||
Using Your Own Event Type
|
Using Your Own Event Type
|
||||||
*************************
|
*************************
|
||||||
@ -339,8 +386,8 @@ handler:
|
|||||||
|
|
||||||
.. code:: haskell
|
.. code:: haskell
|
||||||
|
|
||||||
myEvent :: s -> BrickEvent n CounterEvent -> EventM n (Next s)
|
myEvent :: BrickEvent n CounterEvent -> EventM n s ()
|
||||||
myEvent s (AppEvent (Counter i)) = ...
|
myEvent (AppEvent (Counter i)) = ...
|
||||||
|
|
||||||
The next step is to actually *generate* our custom events and
|
The next step is to actually *generate* our custom events and
|
||||||
inject them into the ``brick`` event stream so they make it to the
|
inject them into the ``brick`` event stream so they make it to the
|
||||||
@ -391,8 +438,8 @@ bound for the event channel. In general, consider the performance of
|
|||||||
your event handler when choosing the channel capacity and design event
|
your event handler when choosing the channel capacity and design event
|
||||||
producers so that they can block if the channel is full.
|
producers so that they can block if the channel is full.
|
||||||
|
|
||||||
appStartEvent: Starting up
|
``appStartEvent``: Starting up
|
||||||
--------------------------
|
------------------------------
|
||||||
|
|
||||||
When an application starts, it may be desirable to perform some of
|
When an application starts, it may be desirable to perform some of
|
||||||
the duties typically only possible when an event has arrived, such as
|
the duties typically only possible when an event has arrived, such as
|
||||||
@ -403,16 +450,17 @@ type provides ``appStartEvent`` function for this purpose:
|
|||||||
|
|
||||||
.. code:: haskell
|
.. code:: haskell
|
||||||
|
|
||||||
appStartEvent :: s -> EventM n s
|
appStartEvent :: EventM n s ()
|
||||||
|
|
||||||
This function takes the initial application state and returns it in
|
This function is a handler action to run on the initial application
|
||||||
``EventM``, possibly changing it and possibly making viewport requests.
|
state. This function is invoked once and only once, at application
|
||||||
This function is invoked once and only once, at application startup.
|
startup. This might be a place to make initial viewport scroll requests
|
||||||
For more details, see `Viewports`_. You will probably just want to use
|
or make changes to the Vty environment. You will probably just want
|
||||||
``return`` as the implementation of this function for most applications.
|
to use ``return ()`` as the implementation of this function for most
|
||||||
|
applications.
|
||||||
|
|
||||||
appChooseCursor: Placing the Cursor
|
``appChooseCursor``: Placing the Cursor
|
||||||
-----------------------------------
|
---------------------------------------
|
||||||
|
|
||||||
The rendering process for a ``Widget`` may return information about
|
The rendering process for a ``Widget`` may return information about
|
||||||
where that widget would like to place the cursor. For example, a text
|
where that widget would like to place the cursor. For example, a text
|
||||||
@ -469,9 +517,10 @@ resource name type ``n`` and would be able to pattern-match on
|
|||||||
|
|
||||||
.. code:: haskell
|
.. code:: haskell
|
||||||
|
|
||||||
myApp = App { ...
|
myApp =
|
||||||
, appChooseCursor = \_ -> showCursorNamed CustomName
|
App { ...
|
||||||
}
|
, appChooseCursor = \_ -> showCursorNamed CustomName
|
||||||
|
}
|
||||||
|
|
||||||
See the next section for more information on using names.
|
See the next section for more information on using names.
|
||||||
|
|
||||||
@ -522,9 +571,8 @@ we don't know which of the two uses of ``Viewport1`` will be affected:
|
|||||||
|
|
||||||
.. code:: haskell
|
.. code:: haskell
|
||||||
|
|
||||||
do
|
let vp = viewportScroll Viewport1
|
||||||
let vp = viewportScroll Viewport1
|
vScrollBy vp 1
|
||||||
vScrollBy vp 1
|
|
||||||
|
|
||||||
The solution is to ensure that for a given resource type (in this case
|
The solution is to ensure that for a given resource type (in this case
|
||||||
viewport), a unique name is assigned in each use.
|
viewport), a unique name is assigned in each use.
|
||||||
@ -537,8 +585,8 @@ viewport), a unique name is assigned in each use.
|
|||||||
ui = (viewport Viewport1 Vertical $ str "Foo") <+>
|
ui = (viewport Viewport1 Vertical $ str "Foo") <+>
|
||||||
(viewport Viewport2 Vertical $ str "Bar") <+>
|
(viewport Viewport2 Vertical $ str "Bar") <+>
|
||||||
|
|
||||||
appAttrMap: Managing Attributes
|
``appAttrMap``: Managing Attributes
|
||||||
-------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
In ``brick`` we use an *attribute map* to assign attributes to elements
|
In ``brick`` we use an *attribute map* to assign attributes to elements
|
||||||
of the interface. Rather than specifying specific attributes when
|
of the interface. Rather than specifying specific attributes when
|
||||||
@ -918,8 +966,8 @@ Attributes`_.
|
|||||||
If the theme is further customized at runtime, any changes can be saved
|
If the theme is further customized at runtime, any changes can be saved
|
||||||
with ``Brick.Themes.saveCustomizations``.
|
with ``Brick.Themes.saveCustomizations``.
|
||||||
|
|
||||||
Wide Character Support and the TextWidth class
|
Wide Character Support and the ``TextWidth`` class
|
||||||
==============================================
|
==================================================
|
||||||
|
|
||||||
Brick attempts to support rendering wide characters in all widgets,
|
Brick attempts to support rendering wide characters in all widgets,
|
||||||
and the brick editor supports entering and editing wide characters.
|
and the brick editor supports entering and editing wide characters.
|
||||||
@ -941,26 +989,28 @@ to support wide characters in your application, this will not work:
|
|||||||
|
|
||||||
let width = Data.Text.length t
|
let width = Data.Text.length t
|
||||||
|
|
||||||
because if the string contains any wide characters, their widths
|
If the string contains any wide characters, their widths will not be
|
||||||
will not be counted properly. In order to get this right, use the
|
counted properly. In order to get this right, use the ``TextWidth`` type
|
||||||
``TextWidth`` type class to compute the width:
|
class to compute the width:
|
||||||
|
|
||||||
.. code:: haskell
|
.. code:: haskell
|
||||||
|
|
||||||
let width = Brick.Widgets.Core.textWidth t
|
let width = Brick.Widgets.Core.textWidth t
|
||||||
|
|
||||||
The ``TextWidth`` type class uses Vty's character width routine
|
The ``TextWidth`` type class uses Vty's character width routine to
|
||||||
to compute the correct width. If you need to compute
|
compute the width by looking up the string's characdters in a Unicode
|
||||||
the width of a single character, use ``Graphics.Text.wcwidth``.
|
width table. If you need to compute the width of a single character, use
|
||||||
|
``Graphics.Text.wcwidth``.
|
||||||
|
|
||||||
Extents
|
Extents
|
||||||
=======
|
=======
|
||||||
|
|
||||||
When an application needs to know where a particular widget was drawn by
|
When an application needs to know where a particular widget was drawn
|
||||||
the renderer, the application can request that the renderer record the
|
by the renderer, the application can request that the renderer record
|
||||||
*extent* of the widget--its upper-left corner and size--and provide it
|
the *extent* of the widget--its upper-left corner and size--and provide
|
||||||
in an event handler. In the following example, the application needs to
|
access to it in an event handler. Extents are represented using Brick's
|
||||||
know where the bordered box containing "Foo" is rendered:
|
``Brick.Types.Extent`` type. In the following example, the application
|
||||||
|
needs to know where the bordered box containing "Foo" is rendered:
|
||||||
|
|
||||||
.. code:: haskell
|
.. code:: haskell
|
||||||
|
|
||||||
@ -979,15 +1029,14 @@ the renderer using a resource name:
|
|||||||
reportExtent FooBox $
|
reportExtent FooBox $
|
||||||
border $ str "Foo"
|
border $ str "Foo"
|
||||||
|
|
||||||
Now, whenever the ``ui`` is rendered, the location and size of the
|
Now, whenever the ``ui`` is rendered, the extent of the bordered box
|
||||||
bordered box containing "Foo" will be recorded. We can then look it up
|
containing "Foo" will be recorded. We can then look it up in event
|
||||||
in event handlers in ``EventM``:
|
handlers in ``EventM``:
|
||||||
|
|
||||||
.. code:: haskell
|
.. code:: haskell
|
||||||
|
|
||||||
do
|
mExtent <- Brick.Main.lookupExtent FooBox
|
||||||
mExtent <- Brick.Main.lookupExtent FooBox
|
case mExtent of
|
||||||
case mExtent of
|
|
||||||
Nothing -> ...
|
Nothing -> ...
|
||||||
Just (Extent _ upperLeft (width, height)) -> ...
|
Just (Extent _ upperLeft (width, height)) -> ...
|
||||||
|
|
||||||
@ -1012,10 +1061,10 @@ to the Vty library handle in ``EventM`` (in e.g. ``appHandleEvent``):
|
|||||||
import qualified Graphics.Vty as V
|
import qualified Graphics.Vty as V
|
||||||
|
|
||||||
do
|
do
|
||||||
vty <- Brick.Main.getVtyHandle
|
vty <- Brick.Main.getVtyHandle
|
||||||
let output = V.outputIface vty
|
let output = V.outputIface vty
|
||||||
when (V.supportsMode output V.BracketedPaste) $
|
when (V.supportsMode output V.BracketedPaste) $
|
||||||
liftIO $ V.setMode output V.BracketedPaste True
|
liftIO $ V.setMode output V.BracketedPaste True
|
||||||
|
|
||||||
Once enabled, paste mode will generate Vty ``EvPaste`` events. These
|
Once enabled, paste mode will generate Vty ``EvPaste`` events. These
|
||||||
events will give you the entire pasted content as a ``ByteString`` which
|
events will give you the entire pasted content as a ``ByteString`` which
|
||||||
@ -1033,10 +1082,10 @@ To enable mouse mode, we need to get access to the Vty library handle in
|
|||||||
.. code:: haskell
|
.. code:: haskell
|
||||||
|
|
||||||
do
|
do
|
||||||
vty <- Brick.Main.getVtyHandle
|
vty <- Brick.Main.getVtyHandle
|
||||||
let output = outputIface vty
|
let output = outputIface vty
|
||||||
when (supportsMode output Mouse) $
|
when (supportsMode output Mouse) $
|
||||||
liftIO $ setMode output Mouse True
|
liftIO $ setMode output Mouse True
|
||||||
|
|
||||||
Bear in mind that some terminals do not support mouse interaction, so
|
Bear in mind that some terminals do not support mouse interaction, so
|
||||||
use Vty's ``getModeStatus`` to find out whether your terminal will
|
use Vty's ``getModeStatus`` to find out whether your terminal will
|
||||||
@ -1057,7 +1106,7 @@ location in the terminal, and any modifier keys pressed.
|
|||||||
|
|
||||||
.. code:: haskell
|
.. code:: haskell
|
||||||
|
|
||||||
handleEvent s (VtyEvent (EvMouseDown col row button mods) = ...
|
handleEvent (VtyEvent (EvMouseDown col row button mods) = ...
|
||||||
|
|
||||||
Brick Mouse Events
|
Brick Mouse Events
|
||||||
------------------
|
------------------
|
||||||
@ -1067,27 +1116,60 @@ a higher-level mouse event interface that ties into the drawing
|
|||||||
language. The disadvantage to the low-level interface described above is
|
language. The disadvantage to the low-level interface described above is
|
||||||
that you still need to determine *what* was clicked, i.e., the part of
|
that you still need to determine *what* was clicked, i.e., the part of
|
||||||
the interface that was under the mouse cursor. There are two ways to do
|
the interface that was under the mouse cursor. There are two ways to do
|
||||||
this with ``brick``: with *extent checking* and *click reporting*.
|
this with ``brick``: with *click reporting* and *extent checking*.
|
||||||
|
|
||||||
|
Click reporting
|
||||||
|
***************
|
||||||
|
|
||||||
|
The *click reporting* approach is the most high-level approach offered
|
||||||
|
by ``brick`` and the one that we recommend you use. In this approach,
|
||||||
|
we use ``Brick.Widgets.Core.clickable`` when drawing the interface to
|
||||||
|
request that a given widget generate ``MouseDown`` and ``MouseUp``
|
||||||
|
events when it is clicked.
|
||||||
|
|
||||||
|
.. code:: haskell
|
||||||
|
|
||||||
|
data Name = MyButton
|
||||||
|
|
||||||
|
ui :: Widget Name
|
||||||
|
ui = center $
|
||||||
|
clickable MyButton $
|
||||||
|
border $
|
||||||
|
str "Click me"
|
||||||
|
|
||||||
|
handleEvent (MouseDown MyButton button modifiers coords) = ...
|
||||||
|
handleEvent (MouseUp MyButton button coords) = ...
|
||||||
|
|
||||||
|
This approach enables event handlers to use pattern matching to check
|
||||||
|
for mouse clicks on specific regions; this uses `Extent checking`_
|
||||||
|
under the hood but makes it possible to denote which widgets are
|
||||||
|
clickable in the interface description. The event's click coordinates
|
||||||
|
are local to the widget being clicked. In the above example, a click
|
||||||
|
on the upper-left corner of the border would result in coordinates of
|
||||||
|
``(0,0)``.
|
||||||
|
|
||||||
Extent checking
|
Extent checking
|
||||||
***************
|
***************
|
||||||
|
|
||||||
The *extent checking* approach entails requesting extents (see
|
The *extent checking* approach entails requesting extents (see
|
||||||
`Extents`_) for parts of your interface, then checking the Vty mouse
|
`Extents`_) for parts of your interface, then checking the Vty mouse
|
||||||
click event's coordinates against one or more extents.
|
click event's coordinates against one or more extents. This approach
|
||||||
|
is slightly lower-level than the direct mouse click reporting approach
|
||||||
|
above but is provided in case you need more control over how mouse
|
||||||
|
clicks are dealt with.
|
||||||
|
|
||||||
The most direct way to do this is to check a specific extent:
|
The most direct way to do this is to check a specific extent:
|
||||||
|
|
||||||
.. code:: haskell
|
.. code:: haskell
|
||||||
|
|
||||||
handleEvent s (VtyEvent (EvMouseDown col row _ _)) = do
|
handleEvent (VtyEvent (EvMouseDown col row _ _)) = do
|
||||||
mExtent <- lookupExtent SomeExtent
|
mExtent <- lookupExtent SomeExtent
|
||||||
case mExtent of
|
case mExtent of
|
||||||
Nothing -> continue s
|
Nothing -> return ()
|
||||||
Just e -> do
|
Just e -> do
|
||||||
if Brick.Main.clickedExtent (col, row) e
|
if Brick.Main.clickedExtent (col, row) e
|
||||||
then ...
|
then ...
|
||||||
else ...
|
else ...
|
||||||
|
|
||||||
This approach works well enough if you know which extent you're
|
This approach works well enough if you know which extent you're
|
||||||
interested in checking, but what if there are many extents and you
|
interested in checking, but what if there are many extents and you
|
||||||
@ -1096,10 +1178,10 @@ different layers? The next approach is to find all clicked extents:
|
|||||||
|
|
||||||
.. code:: haskell
|
.. code:: haskell
|
||||||
|
|
||||||
handleEvent s (VtyEvent (EvMouseDown col row _ _)) = do
|
handleEvent (VtyEvent (EvMouseDown col row _ _)) = do
|
||||||
extents <- Brick.Main.findClickedExtents (col, row)
|
extents <- Brick.Main.findClickedExtents (col, row)
|
||||||
-- Then check to see if a specific extent is in the list, or just
|
-- Then check to see if a specific extent is in the list, or just
|
||||||
-- take the first one in the list.
|
-- take the first one in the list.
|
||||||
|
|
||||||
This approach finds all clicked extents and returns them in a list with
|
This approach finds all clicked extents and returns them in a list with
|
||||||
the following properties:
|
the following properties:
|
||||||
@ -1113,35 +1195,6 @@ the following properties:
|
|||||||
As a result, the extents are ordered in a natural way, starting with the
|
As a result, the extents are ordered in a natural way, starting with the
|
||||||
most specific extents and proceeding to the most general.
|
most specific extents and proceeding to the most general.
|
||||||
|
|
||||||
Click reporting
|
|
||||||
***************
|
|
||||||
|
|
||||||
The *click reporting* approach is the most high-level approach
|
|
||||||
offered by ``brick``. When rendering the interface we use
|
|
||||||
``Brick.Widgets.Core.clickable`` to request that a given widget generate
|
|
||||||
``MouseDown`` and ``MouseUp`` events when it is clicked.
|
|
||||||
|
|
||||||
.. code:: haskell
|
|
||||||
|
|
||||||
data Name = MyButton
|
|
||||||
|
|
||||||
ui :: Widget Name
|
|
||||||
ui = center $
|
|
||||||
clickable MyButton $
|
|
||||||
border $
|
|
||||||
str "Click me"
|
|
||||||
|
|
||||||
handleEvent s (MouseDown MyButton button modifiers coords) = ...
|
|
||||||
handleEvent s (MouseUp MyButton button coords) = ...
|
|
||||||
|
|
||||||
This approach enables event handlers to use pattern matching to check
|
|
||||||
for mouse clicks on specific regions; this uses extent reporting
|
|
||||||
under the hood but makes it possible to denote which widgets are
|
|
||||||
clickable in the interface description. The event's click coordinates
|
|
||||||
are local to the widget being clicked. In the above example, a click
|
|
||||||
on the upper-left corner of the border would result in coordinates of
|
|
||||||
``(0,0)``.
|
|
||||||
|
|
||||||
Viewports
|
Viewports
|
||||||
=========
|
=========
|
||||||
|
|
||||||
@ -1214,14 +1267,14 @@ functions for making scrolling requests:
|
|||||||
|
|
||||||
.. code:: haskell
|
.. code:: haskell
|
||||||
|
|
||||||
hScrollPage :: Direction -> EventM n ()
|
hScrollPage :: Direction -> EventM n s ()
|
||||||
hScrollBy :: Int -> EventM n ()
|
hScrollBy :: Int -> EventM n s ()
|
||||||
hScrollToBeginning :: EventM n ()
|
hScrollToBeginning :: EventM n s ()
|
||||||
hScrollToEnd :: EventM n ()
|
hScrollToEnd :: EventM n s ()
|
||||||
vScrollPage :: Direction -> EventM n ()
|
vScrollPage :: Direction -> EventM n s ()
|
||||||
vScrollBy :: Int -> EventM n ()
|
vScrollBy :: Int -> EventM n s ()
|
||||||
vScrollToBeginning :: EventM n ()
|
vScrollToBeginning :: EventM n s ()
|
||||||
vScrollToEnd :: EventM n ()
|
vScrollToEnd :: EventM n s ()
|
||||||
|
|
||||||
In each case the scrolling function scrolls the viewport by the
|
In each case the scrolling function scrolls the viewport by the
|
||||||
specified amount in the specified direction; functions prefixed with
|
specified amount in the specified direction; functions prefixed with
|
||||||
@ -1237,11 +1290,10 @@ Using ``viewportScroll`` we can write an event handler that scrolls the
|
|||||||
|
|
||||||
.. code:: haskell
|
.. code:: haskell
|
||||||
|
|
||||||
myHandler :: s -> e -> EventM n (Next s)
|
myHandler :: e -> EventM n s ()
|
||||||
myHandler s e = do
|
myHandler e = do
|
||||||
let vp = viewportScroll Viewport1
|
let vp = viewportScroll Viewport1
|
||||||
hScrollBy vp 1
|
hScrollBy vp 1
|
||||||
continue s
|
|
||||||
|
|
||||||
Scrolling Viewports With Visibility Requests
|
Scrolling Viewports With Visibility Requests
|
||||||
--------------------------------------------
|
--------------------------------------------
|
||||||
@ -1473,7 +1525,7 @@ control layout, or change attributes:
|
|||||||
.. code:: haskell
|
.. code:: haskell
|
||||||
|
|
||||||
(str "Name: " <+>) @@=
|
(str "Name: " <+>) @@=
|
||||||
editTextField name NameField (Just 1)
|
editTextField name NameField (Just 1)
|
||||||
|
|
||||||
Now when we invoke ``renderForm`` on a form using the above example,
|
Now when we invoke ``renderForm`` on a form using the above example,
|
||||||
we'll see a ``"Name:"`` label to the left of the editor field for
|
we'll see a ``"Name:"`` label to the left of the editor field for
|
||||||
@ -1508,14 +1560,14 @@ attribute map are:
|
|||||||
Handling Form Events
|
Handling Form Events
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
Handling form events is easy: we just call
|
Handling form events is easy: we just use ``zoom`` to call
|
||||||
``Brick.Forms.handleFormEvent`` with the ``BrickEvent`` and the
|
``Brick.Forms.handleFormEvent`` with the ``BrickEvent`` and a lens
|
||||||
``Form``. This automatically dispatches input events to the
|
to access the ``Form`` in the application state. This automatically
|
||||||
currently-focused input field, and it also manages focus changes with
|
dispatches input events to the currently-focused input field, and it
|
||||||
``Tab`` and ``Shift-Tab`` keybindings. (For details on all of its
|
also manages focus changes with ``Tab`` and ``Shift-Tab`` keybindings.
|
||||||
behaviors, see the Haddock documentation for ``handleFormEvent``.) It's
|
(For details on all of its behaviors, see the Haddock documentation for
|
||||||
still up to the application to decide when events should go to the form
|
``handleFormEvent``.) It's still up to the application to decide when
|
||||||
in the first place.
|
events should go to the form in the first place.
|
||||||
|
|
||||||
Since the form field handlers take ``BrickEvent`` values, that means
|
Since the form field handlers take ``BrickEvent`` values, that means
|
||||||
that custom fields could even handle application-specific events (of the
|
that custom fields could even handle application-specific events (of the
|
||||||
@ -1561,6 +1613,80 @@ For more details on how to do this, see the Haddock documentation for
|
|||||||
the ``FormFieldState`` and ``FormField`` data types along with the
|
the ``FormFieldState`` and ``FormField`` data types along with the
|
||||||
implementations of the built-in form field types.
|
implementations of the built-in form field types.
|
||||||
|
|
||||||
|
Customizable Keybindings
|
||||||
|
========================
|
||||||
|
|
||||||
|
Brick applications typically start out by explicitly checking incoming
|
||||||
|
events for specific keys in ``appHandleEvent``. While this works well
|
||||||
|
enough, it results in *tight coupling* between the input key events and
|
||||||
|
the event handlers that get run. As applications evolve, it becomes
|
||||||
|
important to decouple the input key events and their handlers to allow
|
||||||
|
the input keys to be customized by the user. That's where Brick's
|
||||||
|
customizable keybindings API comes in.
|
||||||
|
|
||||||
|
The customizable keybindings API provides:
|
||||||
|
|
||||||
|
* ``Brick.Keybindings.Parse``: parsing and loading user-provided
|
||||||
|
keybinding configuration files,
|
||||||
|
* ``Brick.Keybindings.Pretty``: pretty-printing keybindings and
|
||||||
|
generating keybinding help text in ``Widget``, plain text, and
|
||||||
|
Markdown formats so you can provide help to users both within the
|
||||||
|
program and outside of it,
|
||||||
|
* ``Brick.Keybindings.KeyEvents``: specifying the application's abstract
|
||||||
|
key events and their configuration names,
|
||||||
|
* ``Brick.Keybindings.KeyConfig``: bundling default and customized
|
||||||
|
keybindings for each abstract event into a structure for use by the
|
||||||
|
dispatcher, and
|
||||||
|
* ``Brick.Keybindings.KeyDispatcher``: specifying handlers and
|
||||||
|
dispatching incoming key events to them.
|
||||||
|
|
||||||
|
This section of the User Guide describes the API at a high level,
|
||||||
|
but Brick also provides a complete working example of the custom
|
||||||
|
keybinding API in ``programs/CustomKeybindingDemo.hs`` and
|
||||||
|
provides detailed documentation on how to use the API, including a
|
||||||
|
step-by-step process for using it, in the module documentation for
|
||||||
|
``Brick.Keybindings.KeyDispatcher``.
|
||||||
|
|
||||||
|
The following table compares Brick application design decisions and
|
||||||
|
runtime behaviors in a typical application compared to one that uses the
|
||||||
|
customizable keybindings API:
|
||||||
|
|
||||||
|
+---------------------+------------------------+-------------------------+
|
||||||
|
| **Approach** | **Before runtime** | **At runtime** |
|
||||||
|
+---------------------+------------------------+-------------------------+
|
||||||
|
| Typical application | The application author | #. An input event |
|
||||||
|
| (no custom | decides which keys will| arrives when the user|
|
||||||
|
| keybindings) | trigger application | presses a key. |
|
||||||
|
| | behaviors. The event | #. The event handler |
|
||||||
|
| | handler is written to | pattern-mathces on |
|
||||||
|
| | pattern-match on | the input event to |
|
||||||
|
| | specific keys. | check for a match and|
|
||||||
|
| | | then runs the |
|
||||||
|
| | | corresponding |
|
||||||
|
| | | handler. |
|
||||||
|
+---------------------+------------------------+-------------------------+
|
||||||
|
| Application with | The application author | #. A Vty input event |
|
||||||
|
| custom keybindings | specifies the possible | arrives when the user|
|
||||||
|
| API integrated | *abstract events* that | presses a key. |
|
||||||
|
| | correspond to the | #. The input event is |
|
||||||
|
| | application's | provided to |
|
||||||
|
| | behaviors. The events | ``appHandleEvent``. |
|
||||||
|
| | are given default | #. ``appHandleEvent`` |
|
||||||
|
| | keybindings. The | passes the event on |
|
||||||
|
| | application provides | to a |
|
||||||
|
| | event handlers for the | ``KeyDispatcher``. |
|
||||||
|
| | abstract events, not | #. The key dispatcher |
|
||||||
|
| | specific keys. If | checks to see whether|
|
||||||
|
| | desired, the | the input key event |
|
||||||
|
| | application can load | maps to an abstract |
|
||||||
|
| | user-defined custom | event. |
|
||||||
|
| | keybindings from an INI| #. If the dispatcher |
|
||||||
|
| | file at startup to | finds a match, the |
|
||||||
|
| | override the | corresponding |
|
||||||
|
| | application's defaults.| abstract event's key |
|
||||||
|
| | | handler is run. |
|
||||||
|
+---------------------+------------------------+-------------------------+
|
||||||
|
|
||||||
Joining Borders
|
Joining Borders
|
||||||
===============
|
===============
|
||||||
|
|
||||||
@ -1782,12 +1908,12 @@ use the cache invalidation functions in ``EventM``:
|
|||||||
|
|
||||||
.. code:: haskell
|
.. code:: haskell
|
||||||
|
|
||||||
handleEvent s ... = do
|
handleEvent ... = do
|
||||||
-- Invalidate just a single cache entry:
|
-- Invalidate just a single cache entry:
|
||||||
Brick.Main.invalidateCacheEntry ExpensiveThing
|
Brick.Main.invalidateCacheEntry ExpensiveThing
|
||||||
|
|
||||||
-- Invalidate the entire cache (useful on a resize):
|
-- Invalidate the entire cache (useful on a resize):
|
||||||
Brick.Main.invalidateCache
|
Brick.Main.invalidateCache
|
||||||
|
|
||||||
Implementing Custom Widgets
|
Implementing Custom Widgets
|
||||||
===========================
|
===========================
|
||||||
|
@ -1,546 +0,0 @@
|
|||||||
|
|
||||||
# Brick Tutorial by Samuel Tay
|
|
||||||
|
|
||||||
This tutorial was written by Samuel Tay, Copyright 2017
|
|
||||||
(https://github.com/samtay, https://samtay.github.io/). It is provided
|
|
||||||
as part of the brick distribution with permission.
|
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
I'm going to give a short introduction to
|
|
||||||
[brick](https://hackage.haskell.org/package/brick), a Haskell library
|
|
||||||
for building terminal user interfaces. So far I've used `brick` to
|
|
||||||
implement [Conway's Game of Life](https://github.com/samtay/conway) and
|
|
||||||
a [Tetris clone](https://github.com/samtay/tetris). I'll explain the
|
|
||||||
basics, walk through an example [snake](https://github.com/samtay/snake)
|
|
||||||
application, and then explain some more complicated scenarios.
|
|
||||||
|
|
||||||
The first thing I'll say is that this package has some of the most
|
|
||||||
impressive documentation and resources, which makes it easy to figure
|
|
||||||
out pretty much anything you need to do. I'll try to make this useful,
|
|
||||||
but I imagine if you're reading this then it is mostly being used as a
|
|
||||||
reference in addition to the existing resources:
|
|
||||||
|
|
||||||
1. [Demo programs](https://github.com/jtdaugherty/brick/tree/master/programs)
|
|
||||||
(clone down to explore the code and run them locally)
|
|
||||||
2. [User guide](https://github.com/jtdaugherty/brick/blob/master/docs/guide.rst)
|
|
||||||
3. [Haddock docs](https://hackage.haskell.org/package/brick)
|
|
||||||
4. [Google group](https://groups.google.com/forum/#!forum/brick-users)
|
|
||||||
|
|
||||||
### The basic idea
|
|
||||||
|
|
||||||
`brick` is very declarative. Once your base application logic is in
|
|
||||||
place, the interface is generally built by two functions: drawing and
|
|
||||||
handling events. The drawing function
|
|
||||||
|
|
||||||
```haskell
|
|
||||||
appDraw :: s -> [Widget n]
|
|
||||||
```
|
|
||||||
|
|
||||||
takes your app state `s` and produces the visuals `[Widget n]`. The
|
|
||||||
handler
|
|
||||||
|
|
||||||
```haskell
|
|
||||||
appHandleEvent :: s -> BrickEvent n e -> EventM n (Next s)
|
|
||||||
```
|
|
||||||
|
|
||||||
takes your app state, an event (e.g. user presses the `'m'` key), and
|
|
||||||
produces the resulting app state. *That's pretty much it.*
|
|
||||||
|
|
||||||
## `snake`
|
|
||||||
|
|
||||||
We're going to build the [classic
|
|
||||||
snake](https://en.wikipedia.org/wiki/Snake_(video_game)) game that you
|
|
||||||
might recall from arcades or the first cell phones. The full source code
|
|
||||||
is [here](https://github.com/samtay/snake). This is the end product:
|
|
||||||
|
|
||||||
![](snake-demo.gif)
|
|
||||||
|
|
||||||
### Structure of the app
|
|
||||||
|
|
||||||
The library makes it easy to separate the concerns of your application
|
|
||||||
and the interface; I like to have a module with all of the core business
|
|
||||||
logic that exports the core state of the app and functions for modifying
|
|
||||||
it, and then have an interface module that just handles the setup,
|
|
||||||
drawing, and handling events. So let's just use the `simple` stack
|
|
||||||
template and add two modules
|
|
||||||
|
|
||||||
```
|
|
||||||
├── LICENSE
|
|
||||||
├── README.md
|
|
||||||
├── Setup.hs
|
|
||||||
├── snake.cabal
|
|
||||||
├── src
|
|
||||||
│ ├── Main.hs
|
|
||||||
│ ├── Snake.hs
|
|
||||||
│ └── UI.hs
|
|
||||||
└── stack.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
and our dependencies to `test.cabal`
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
executable snake
|
|
||||||
hs-source-dirs: src
|
|
||||||
main-is: Main.hs
|
|
||||||
ghc-options: -threaded
|
|
||||||
exposed-modules: Snake
|
|
||||||
, UI
|
|
||||||
default-language: Haskell2010
|
|
||||||
build-depends: base >= 4.7 && < 5
|
|
||||||
, brick
|
|
||||||
, containers
|
|
||||||
, linear
|
|
||||||
, microlens
|
|
||||||
, microlens-th
|
|
||||||
, random
|
|
||||||
```
|
|
||||||
|
|
||||||
### `Snake`
|
|
||||||
|
|
||||||
Since this tutorial is about `brick`, I'll elide most of the
|
|
||||||
implementation details of the actual game, but here are some of the key
|
|
||||||
types and scaffolding:
|
|
||||||
|
|
||||||
```haskell
|
|
||||||
{-# LANGUAGE TemplateHaskell, FlexibleContexts #-}
|
|
||||||
module Snake where
|
|
||||||
|
|
||||||
import Control.Applicative ((<|>))
|
|
||||||
import Control.Monad (guard)
|
|
||||||
import Data.Maybe (fromMaybe)
|
|
||||||
|
|
||||||
import Data.Sequence (Seq, ViewL(..), ViewR(..), (<|))
|
|
||||||
import qualified Data.Sequence as S
|
|
||||||
import Lens.Micro.TH (makeLenses)
|
|
||||||
import Lens.Micro ((&), (.~), (%~), (^.))
|
|
||||||
import Linear.V2 (V2(..), _x, _y)
|
|
||||||
import System.Random (Random(..), newStdGen)
|
|
||||||
|
|
||||||
-- Types
|
|
||||||
|
|
||||||
data Game = Game
|
|
||||||
{ _snake :: Snake -- ^ snake as a sequence of points in R2
|
|
||||||
, _dir :: Direction -- ^ direction
|
|
||||||
, _food :: Coord -- ^ location of the food
|
|
||||||
, _foods :: Stream Coord -- ^ infinite list of random food locations
|
|
||||||
, _dead :: Bool -- ^ game over flag
|
|
||||||
, _paused :: Bool -- ^ paused flag
|
|
||||||
, _score :: Int -- ^ score
|
|
||||||
, _frozen :: Bool -- ^ freeze to disallow duplicate turns
|
|
||||||
} deriving (Show)
|
|
||||||
|
|
||||||
type Coord = V2 Int
|
|
||||||
type Snake = Seq Coord
|
|
||||||
|
|
||||||
data Stream a = a :| Stream a
|
|
||||||
deriving (Show)
|
|
||||||
|
|
||||||
data Direction
|
|
||||||
= North
|
|
||||||
| South
|
|
||||||
| East
|
|
||||||
| West
|
|
||||||
deriving (Eq, Show)
|
|
||||||
```
|
|
||||||
|
|
||||||
All of this is pretty self-explanatory, with the possible exception
|
|
||||||
of lenses if you haven't seen them. At first glance they may seem
|
|
||||||
complicated (and the underlying theory arguably is), but using them as
|
|
||||||
getters and setters is very straightforward. So, if you are following
|
|
||||||
along because you are writing a terminal app like this, I'd recommend
|
|
||||||
using them, but they are not required to use `brick`.
|
|
||||||
|
|
||||||
Here are the core functions for playing the game:
|
|
||||||
|
|
||||||
```haskell
|
|
||||||
-- | Step forward in time
|
|
||||||
step :: Game -> Game
|
|
||||||
step g = fromMaybe g $ do
|
|
||||||
guard (not $ g ^. paused || g ^. dead)
|
|
||||||
let g' = g & frozen .~ False
|
|
||||||
return . fromMaybe (move g') $ die g' <|> eatFood g'
|
|
||||||
|
|
||||||
-- | Possibly die if next head position is disallowed
|
|
||||||
die :: Game -> Maybe Game
|
|
||||||
|
|
||||||
-- | Possibly eat food if next head position is food
|
|
||||||
eatFood :: Game -> Maybe Game
|
|
||||||
|
|
||||||
-- | Move snake along in a marquee fashion
|
|
||||||
move :: Game -> Game
|
|
||||||
|
|
||||||
-- | Turn game direction (only turns orthogonally)
|
|
||||||
--
|
|
||||||
-- Implicitly unpauses yet freezes game
|
|
||||||
turn :: Direction -> Game -> Game
|
|
||||||
|
|
||||||
-- | Initialize a paused game with random food location
|
|
||||||
initGame :: IO Game
|
|
||||||
```
|
|
||||||
|
|
||||||
### `UI`
|
|
||||||
|
|
||||||
To start, we need to determine what our `App s e n` type parameters are.
|
|
||||||
This will completely describe the interface application and be passed
|
|
||||||
to one of the library's `main` style functions for execution. Note that
|
|
||||||
`s` is the app state, `e` is an event type, and `n` is a resource name.
|
|
||||||
The `e` is abstracted so that we can provide custom events. The `n`
|
|
||||||
is usually a custom sum type called `Name` which allows us to *name*
|
|
||||||
particular viewports. This is important so that we can keep track of
|
|
||||||
where the user currently has *focus*, such as typing in one of two
|
|
||||||
textboxes; however, for this simple snake game we don't need to worry
|
|
||||||
about that.
|
|
||||||
|
|
||||||
In simpler cases, the state `s` can directly coincide with a core
|
|
||||||
datatype such as our `Snake.Game`. In many cases however, it will be
|
|
||||||
necessary to wrap the core state within the ui state `s` to keep track
|
|
||||||
of things that are interface specific (more on this later).
|
|
||||||
|
|
||||||
Let's write out our app definition and leave some undefined functions:
|
|
||||||
|
|
||||||
```haskell
|
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
module UI where
|
|
||||||
|
|
||||||
import Control.Monad (forever, void)
|
|
||||||
import Control.Monad.IO.Class (liftIO)
|
|
||||||
import Control.Concurrent (threadDelay, forkIO)
|
|
||||||
import Data.Maybe (fromMaybe)
|
|
||||||
|
|
||||||
import Snake
|
|
||||||
|
|
||||||
import Brick
|
|
||||||
( App(..), AttrMap, BrickEvent(..), EventM, Next, Widget
|
|
||||||
, customMain, neverShowCursor
|
|
||||||
, continue, halt
|
|
||||||
, hLimit, vLimit, vBox, hBox
|
|
||||||
, padRight, padLeft, padTop, padAll, Padding(..)
|
|
||||||
, withBorderStyle
|
|
||||||
, str
|
|
||||||
, attrMap, withAttr, emptyWidget, AttrName, on, fg
|
|
||||||
, (<+>)
|
|
||||||
)
|
|
||||||
import Brick.BChan (newBChan, writeBChan)
|
|
||||||
import qualified Brick.Widgets.Border as B
|
|
||||||
import qualified Brick.Widgets.Border.Style as BS
|
|
||||||
import qualified Brick.Widgets.Center as C
|
|
||||||
import qualified Graphics.Vty as V
|
|
||||||
import Data.Sequence (Seq)
|
|
||||||
import qualified Data.Sequence as S
|
|
||||||
import Linear.V2 (V2(..))
|
|
||||||
import Lens.Micro ((^.))
|
|
||||||
|
|
||||||
-- Types
|
|
||||||
|
|
||||||
-- | Ticks mark passing of time
|
|
||||||
--
|
|
||||||
-- This is our custom event that will be constantly fed into the app.
|
|
||||||
data Tick = Tick
|
|
||||||
|
|
||||||
-- | Named resources
|
|
||||||
--
|
|
||||||
-- Not currently used, but will be easier to refactor
|
|
||||||
-- if we call this "Name" now.
|
|
||||||
type Name = ()
|
|
||||||
|
|
||||||
data Cell = Snake | Food | Empty
|
|
||||||
|
|
||||||
-- App definition
|
|
||||||
|
|
||||||
app :: App Game Tick Name
|
|
||||||
app = App { appDraw = drawUI
|
|
||||||
, appChooseCursor = neverShowCursor
|
|
||||||
, appHandleEvent = handleEvent
|
|
||||||
, appStartEvent = return
|
|
||||||
, appAttrMap = const theMap
|
|
||||||
}
|
|
||||||
|
|
||||||
main :: IO ()
|
|
||||||
main = undefined
|
|
||||||
|
|
||||||
-- Handling events
|
|
||||||
|
|
||||||
handleEvent :: Game -> BrickEvent Name Tick -> EventM Name (Next Game)
|
|
||||||
handleEvent = undefined
|
|
||||||
|
|
||||||
-- Drawing
|
|
||||||
|
|
||||||
drawUI :: Game -> [Widget Name]
|
|
||||||
drawUI = undefined
|
|
||||||
|
|
||||||
theMap :: AttrMap
|
|
||||||
theMap = undefined
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Custom Events
|
|
||||||
|
|
||||||
So far I've only used `brick` to make games which need to be redrawn
|
|
||||||
as time passes, with or without user input. This requires using
|
|
||||||
`Brick.customMain` with that `Tick` event type, and opening a forked
|
|
||||||
process to `forever` feed that event type into the channel. Since this
|
|
||||||
is a common scenario, there is a `Brick.BChan` module that makes this
|
|
||||||
pretty quick:
|
|
||||||
|
|
||||||
```haskell
|
|
||||||
main :: IO ()
|
|
||||||
main = do
|
|
||||||
chan <- newBChan 10
|
|
||||||
forkIO $ forever $ do
|
|
||||||
writeBChan chan Tick
|
|
||||||
threadDelay 100000 -- decides how fast your game moves
|
|
||||||
g <- initGame
|
|
||||||
let buildVty = V.mkVty V.defaultConfig
|
|
||||||
initialVty <- buildVty
|
|
||||||
void $ customMain initialVty buildVty (Just chan) app g
|
|
||||||
```
|
|
||||||
|
|
||||||
We do need to import `Vty.Graphics` since `customMain` allows us
|
|
||||||
to specify a custom `IO Vty.Graphics.Vty` handle, but we're only
|
|
||||||
customizing the existence of the event channel `BChan Tick`. The app
|
|
||||||
is now bootstrapped, and all we need to do is implement `handleEvent`,
|
|
||||||
`drawUI`, and `theMap` (handles styling).
|
|
||||||
|
|
||||||
#### Handling events
|
|
||||||
|
|
||||||
Handling events is largely straightforward, and can be very clean when
|
|
||||||
your underlying application logic is taken care of in a core module. All
|
|
||||||
we do is essentially map events to the proper state modifiers.
|
|
||||||
|
|
||||||
```haskell
|
|
||||||
handleEvent :: Game -> BrickEvent Name Tick -> EventM Name (Next Game)
|
|
||||||
handleEvent g (AppEvent Tick) = continue $ step g
|
|
||||||
handleEvent g (VtyEvent (V.EvKey V.KUp [])) = continue $ turn North g
|
|
||||||
handleEvent g (VtyEvent (V.EvKey V.KDown [])) = continue $ turn South g
|
|
||||||
handleEvent g (VtyEvent (V.EvKey V.KRight [])) = continue $ turn East g
|
|
||||||
handleEvent g (VtyEvent (V.EvKey V.KLeft [])) = continue $ turn West g
|
|
||||||
handleEvent g (VtyEvent (V.EvKey (V.KChar 'k') [])) = continue $ turn North g
|
|
||||||
handleEvent g (VtyEvent (V.EvKey (V.KChar 'j') [])) = continue $ turn South g
|
|
||||||
handleEvent g (VtyEvent (V.EvKey (V.KChar 'l') [])) = continue $ turn East g
|
|
||||||
handleEvent g (VtyEvent (V.EvKey (V.KChar 'h') [])) = continue $ turn West g
|
|
||||||
handleEvent g (VtyEvent (V.EvKey (V.KChar 'r') [])) = liftIO (initGame) >>= continue
|
|
||||||
handleEvent g (VtyEvent (V.EvKey (V.KChar 'q') [])) = halt g
|
|
||||||
handleEvent g (VtyEvent (V.EvKey V.KEsc [])) = halt g
|
|
||||||
handleEvent g _ = continue g
|
|
||||||
```
|
|
||||||
|
|
||||||
It's probably obvious, but `continue` will continue execution with
|
|
||||||
the supplied state value, which is then drawn. We can also `halt` to
|
|
||||||
stop execution, which will essentially finish the evaluation of our
|
|
||||||
`customMain` and result in `IO Game`, where the resulting game is the
|
|
||||||
last value that we supplied to `halt`.
|
|
||||||
|
|
||||||
#### Drawing
|
|
||||||
|
|
||||||
Drawing is fairly simple as well but can require a good amount of code
|
|
||||||
to position things how you want them. I like to break up the visual
|
|
||||||
space into regions with drawing functions for each one.
|
|
||||||
|
|
||||||
```haskell
|
|
||||||
drawUI :: Game -> [Widget Name]
|
|
||||||
drawUI g =
|
|
||||||
[ C.center $ padRight (Pad 2) (drawStats g) <+> drawGrid g ]
|
|
||||||
|
|
||||||
drawStats :: Game -> Widget Name
|
|
||||||
drawStats = undefined
|
|
||||||
|
|
||||||
drawGrid :: Game -> Widget Name
|
|
||||||
drawGrid = undefined
|
|
||||||
```
|
|
||||||
|
|
||||||
This will center the overall interface (`C.center`), put the stats and
|
|
||||||
grid widgets horizontally side by side (`<+>`), and separate them by a
|
|
||||||
2-character width (`padRight (Pad 2)`).
|
|
||||||
|
|
||||||
Let's move forward with the stats column:
|
|
||||||
|
|
||||||
```haskell
|
|
||||||
drawStats :: Game -> Widget Name
|
|
||||||
drawStats g = hLimit 11
|
|
||||||
$ vBox [ drawScore (g ^. score)
|
|
||||||
, padTop (Pad 2) $ drawGameOver (g ^. dead)
|
|
||||||
]
|
|
||||||
|
|
||||||
drawScore :: Int -> Widget Name
|
|
||||||
drawScore n = withBorderStyle BS.unicodeBold
|
|
||||||
$ B.borderWithLabel (str "Score")
|
|
||||||
$ C.hCenter
|
|
||||||
$ padAll 1
|
|
||||||
$ str $ show n
|
|
||||||
|
|
||||||
drawGameOver :: Bool -> Widget Name
|
|
||||||
drawGameOver dead =
|
|
||||||
if dead
|
|
||||||
then withAttr gameOverAttr $ C.hCenter $ str "GAME OVER"
|
|
||||||
else emptyWidget
|
|
||||||
|
|
||||||
gameOverAttr :: AttrName
|
|
||||||
gameOverAttr = "gameOver"
|
|
||||||
```
|
|
||||||
|
|
||||||
I'm throwing in that `hLimit 11` to prevent the widget greediness caused
|
|
||||||
by the outer `C.center`. I'm also using `vBox` to show some other
|
|
||||||
options of aligning widgets; `vBox` and `hBox` align a list of widgets
|
|
||||||
vertically and horizontally, respectfully. They can be thought of as
|
|
||||||
folds over the binary `<=>` and `<+>` operations.
|
|
||||||
|
|
||||||
The score is straightforward, but it is the first border in
|
|
||||||
this tutorial. Borders are well documented in the [border
|
|
||||||
demo](https://github.com/jtdaugherty/brick/blob/master/programs/BorderDemo.hs)
|
|
||||||
and the Haddocks for that matter.
|
|
||||||
|
|
||||||
We also only show the "game over" widget if the game is actually over.
|
|
||||||
In that case, we are rendering the string widget with the `gameOverAttr`
|
|
||||||
attribute name. Attribute names are basically type safe *names* that
|
|
||||||
we can assign to widgets to apply predetermined styles, similar to
|
|
||||||
assigning a class name to a div in HTML and defining the CSS styles for
|
|
||||||
that class elsewhere.
|
|
||||||
|
|
||||||
Attribute names implement `IsString`, so they are easy to construct with
|
|
||||||
the `OverloadedStrings` pragma.
|
|
||||||
|
|
||||||
Now for the main event:
|
|
||||||
|
|
||||||
```haskell
|
|
||||||
drawGrid :: Game -> Widget Name
|
|
||||||
drawGrid g = withBorderStyle BS.unicodeBold
|
|
||||||
$ B.borderWithLabel (str "Snake")
|
|
||||||
$ vBox rows
|
|
||||||
where
|
|
||||||
rows = [hBox $ cellsInRow r | r <- [height-1,height-2..0]]
|
|
||||||
cellsInRow y = [drawCoord (V2 x y) | x <- [0..width-1]]
|
|
||||||
drawCoord = drawCell . cellAt
|
|
||||||
cellAt c
|
|
||||||
| c `elem` g ^. snake = Snake
|
|
||||||
| c == g ^. food = Food
|
|
||||||
| otherwise = Empty
|
|
||||||
|
|
||||||
drawCell :: Cell -> Widget Name
|
|
||||||
drawCell Snake = withAttr snakeAttr cw
|
|
||||||
drawCell Food = withAttr foodAttr cw
|
|
||||||
drawCell Empty = withAttr emptyAttr cw
|
|
||||||
|
|
||||||
cw :: Widget Name
|
|
||||||
cw = str " "
|
|
||||||
|
|
||||||
snakeAttr, foodAttr, emptyAttr :: AttrName
|
|
||||||
snakeAttr = "snakeAttr"
|
|
||||||
foodAttr = "foodAttr"
|
|
||||||
emptyAttr = "emptyAttr"
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
There's actually nothing new here! We've already covered all the
|
|
||||||
`brick` functions necessary to draw the grid. My approach to grids is
|
|
||||||
to render a square cell widget `cw` with different colors depending
|
|
||||||
on the cell state. The easiest way to draw a colored square is to
|
|
||||||
stick two characters side by side. If we assign an attribute with a
|
|
||||||
matching foreground and background, then it doesn't matter what the two
|
|
||||||
characters are (provided that they aren't some crazy Unicode characters
|
|
||||||
that might render to an unexpected size). However, if we want empty
|
|
||||||
cells to render with the same color as the user's default background
|
|
||||||
color, then spaces are a good choice.
|
|
||||||
|
|
||||||
Finally, we'll define the attribute map:
|
|
||||||
|
|
||||||
```haskell
|
|
||||||
theMap :: AttrMap
|
|
||||||
theMap = attrMap V.defAttr
|
|
||||||
[ (snakeAttr, V.blue `on` V.blue)
|
|
||||||
, (foodAttr, V.red `on` V.red)
|
|
||||||
, (gameOverAttr, fg V.red `V.withStyle` V.bold)
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Again, styles aren't terribly complicated, but it
|
|
||||||
will be one area where you might have to look in the
|
|
||||||
[vty](http://hackage.haskell.org/package/vty) package (specifically
|
|
||||||
[Graphics.Vty.Attributes](http://hackage.haskell.org/package/vty-5.15.1/docs/Graphics-Vty-Attributes.html)) to find what you need.
|
|
||||||
|
|
||||||
Another thing to mention is that the attributes form a hierarchy and
|
|
||||||
can be combined in a parent-child relationship via `mappend`. I haven't
|
|
||||||
actually used this feature, but it does sound quite handy. For a more
|
|
||||||
detailed discussion see the
|
|
||||||
[Brick.AttrMap](https://hackage.haskell.org/package/brick-0.18/docs/Brick-AttrMap.html) haddocks.
|
|
||||||
|
|
||||||
## Variable speed
|
|
||||||
|
|
||||||
One difficult problem I encountered was implementing a variable speed in
|
|
||||||
the GoL. I could have just used the same approach above with the minimum
|
|
||||||
thread delay (corresponding to the maximum speed) and counted `Tick`
|
|
||||||
events, only issuing an actual `step` in the game when the modular count
|
|
||||||
of `Tick`s reached an amount corresponding to the current game speed,
|
|
||||||
but that's kind of an ugly approach.
|
|
||||||
|
|
||||||
Instead, I reached out to the author and he advised me to use a `TVar`
|
|
||||||
within the app state. I had never used `TVar`, but it's pretty easy!
|
|
||||||
|
|
||||||
```haskell
|
|
||||||
main :: IO ()
|
|
||||||
main = do
|
|
||||||
chan <- newBChan 10
|
|
||||||
tv <- atomically $ newTVar (spToInt initialSpeed)
|
|
||||||
forkIO $ forever $ do
|
|
||||||
writeBChan chan Tick
|
|
||||||
int <- atomically $ readTVar tv
|
|
||||||
threadDelay int
|
|
||||||
let buildVty = V.mkVty V.defaultConfig
|
|
||||||
initialVty <- buildVty
|
|
||||||
customMain initialVty buildVty (Just chan) app (initialGame tv)
|
|
||||||
>>= printResult
|
|
||||||
```
|
|
||||||
|
|
||||||
The `tv <- atomically $ newTVar (value :: a)` creates a new mutable
|
|
||||||
reference to a value of type `a`, i.e. `TVar a`, and returns it in `IO`.
|
|
||||||
In this case `value` is an `Int` which represents the delay between game
|
|
||||||
steps. Then in the forked process, we read the delay from the `TVar`
|
|
||||||
reference and use that to space out the calls to `writeBChan chan Tick`.
|
|
||||||
|
|
||||||
I store that same `tv :: TVar Int` in the brick app state, so that the
|
|
||||||
user can change the speed:
|
|
||||||
|
|
||||||
```haskell
|
|
||||||
handleEvent :: Game -> BrickEvent Name Tick -> EventM Name (Next Game)
|
|
||||||
handleEvent g (VtyEvent (V.EvKey V.KRight [V.MCtrl])) = handleSpeed g (+)
|
|
||||||
handleEvent g (VtyEvent (V.EvKey V.KLeft [V.MCtrl])) = handleSpeed g (-)
|
|
||||||
|
|
||||||
handleSpeed :: Game -> (Float -> Float -> Float) -> EventM n (Next Game)
|
|
||||||
handleSpeed g (+/-) = do
|
|
||||||
let newSp = validS $ (g ^. speed) +/- speedInc
|
|
||||||
liftIO $ atomically $ writeTVar (g ^. interval) (spToInt newSp)
|
|
||||||
continue $ g & speed .~ newSp
|
|
||||||
|
|
||||||
-- where
|
|
||||||
|
|
||||||
-- | Speed increments = 0.01 gives 100 discrete speed settings
|
|
||||||
speedInc :: Float
|
|
||||||
speedInc = 0.01
|
|
||||||
|
|
||||||
-- | Game state
|
|
||||||
data Game = Game
|
|
||||||
{ _board :: Board -- ^ Board state
|
|
||||||
, _time :: Int -- ^ Time elapsed
|
|
||||||
, _paused :: Bool -- ^ Playing vs. paused
|
|
||||||
, _speed :: Float -- ^ Speed in [0..1]
|
|
||||||
, _interval :: TVar Int -- ^ Interval kept in TVar
|
|
||||||
, _focus :: F.FocusRing Name -- ^ Keeps track of grid focus
|
|
||||||
, _selected :: Cell -- ^ Keeps track of cell focus
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
`brick` let's you build TUIs very quickly. I was able to write `snake`
|
|
||||||
along with this tutorial within a few hours. More complicated interfaces
|
|
||||||
can be tougher, but if you can successfully separate the interface and
|
|
||||||
core functionality, you'll have an easier time tacking on the frontend.
|
|
||||||
|
|
||||||
Lastly, let me remind you to look in the
|
|
||||||
[demo programs](https://github.com/jtdaugherty/brick/tree/master/programs)
|
|
||||||
to figure stuff out, as *many* scenarios are covered throughout them.
|
|
||||||
|
|
||||||
## Links
|
|
||||||
* [brick](https://hackage.haskell.org/package/brick)
|
|
||||||
* [snake](https://github.com/samtay/snake)
|
|
||||||
* [tetris](https://github.com/samtay/tetris)
|
|
||||||
* [conway](https://github.com/samtay/conway)
|
|
Loading…
Reference in New Issue
Block a user