Merge branch 'documentation/brick-1.0-staging'

This commit is contained in:
Jonathan Daugherty 2022-08-08 15:29:00 -07:00
commit 2b467f55e3
5 changed files with 422 additions and 765 deletions

View File

@ -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
---- ----

View File

@ -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)

View File

@ -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,

View File

@ -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
=========================== ===========================

View File

@ -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)