packages feed

brick-panes-1.0.0.4: README.org

* Overview

The Brick Panes library is an overlay for the Brick TUI (Text User Interface)
library that allows individual TUI screen areas to be independently developed and
then easily composed into the overall application.  This library can be used to
develop an application in a modular fashion where the screen is divided into a
number of "panes" that individually handle their display and (optionally) their
events with pane-specific internal state.  These discrete panes provide a general
interface that can then easily be composed to provide the wholistic application
functionality.

* Context

Brick is oriented around the display of and interaction with ~Widgets~.  This a
nice overall design and lends itself well to discrete management of different
types of TUI functionality.  The panes library builds on top of Brick Widgets to
address the following higher-level compositional issues encountered when building
a TUI application:

1. Different Widgets have different interfaces.  A Widget must be called via a
   widget-specific API.  Fundamentally however, there are a number of common
   higher-level operations that an application performs for which the details are
   a lower-level concern.

2. Various application functionality can be isolated into different aspects, but
   frequently the individual aspects are made up of multiple, cooperating widgets
   that share some state.  Managing this state and coordinating the activities of
   an inter-Widget aspect can be done via application-specific wrapper Widgets,
   but often it's desirable to view ~Widgets~ as the /basic/ building blocks
   (er.. "bricks") of the application and to then have a higher level of
   abstraction for handling the different aspect "areas" of the application.

This library provides the ~Pane~ as self-contained collector for Widgets that
manage a particular aspect of the application within the global context of the
entire application.  It also provides a ~Panel~, which is a composition of
multiple ~Pane~ objects that maintains a simple general API while internally
managing the ~Pane~ objects which compose the ~Panel~.

The Haddock documentation describes the API, and the remainder of this README is
an evolutionary introduction to developing an application with Panes.

* Example Program

To introduce the Panes library in a manner that reveals the motivation and
interaction in a layered approach, the documentation here will demonstrate
building a sample application step-by-step.

The sample application that will be developed here is called "mywork-example" and
is designed to help the software developer keep track of their work.  This is a
simple little application that reads an input JSON file that provides information
about different projects the developer is working on and then allows the
developer to browse and manage the information about those projects.

#+begin_quote
Note: there is a more complete implementation of this example at
https://github.com/kquick/mywork, which has many more features, but which is no
longer suitable for simple demonstration purposes.  The ~mywork-example~ is
incomplete and not fully functional, and users who would like to see or use the
more featureful implementation should use the one at the above link.
#+end_quote

The underlying data for the mywork-example application consists of a list of Projects,
where each Project has a top-level description.  For each project, there are a
number of Locations, which represents various locations the project can be found
at; the Locations can include local directories, code repositories (e.g. github),
and publication sites (e.g. Hackage).  Each Location can have a date associated
with it (statically or dynamically) as well as user-specified Notes.  In
addition, the files in a project can be viewed in a simple scrollable region.

Let's start by defining the underlying data structures that will be used to
represent those projects in memory, and which can be serialized to a JSON storage
file:

#+begin_src haskell
import           Data.Text ( Text )
import           Data.Time.Calendar


newtype Projects = Projects { projects :: [Project] }
  deriving Generic

data Project = Project { name :: Text
                       , role :: Role
                       , description :: Text
                       , language :: Either Text Language
                       , locations :: [Location]
                       }
  deriving Generic

data Role = Author | Maintainer | Contributor | User
  deriving (Show, Enum, Bounded, Eq, Generic)

data Language = Haskell | Rust | C | CPlusPlus | Python | JavaScript
  deriving (Eq, Generic)

data Location = Location { location :: Text
                         , locatedOn :: Maybe Day
                         , notes :: [Note]
                         }
  deriving Generic

data Note = Note { note :: Text
                 , notedOn :: Day
                 }
  deriving Generic
#+end_src

Each of these definitions will have an ~aeson~ ~ToJSON~ derivation so that our
projects can be read from and written to a local JSON-format file.

  #+begin_quote
  Note that the ~ToJSON~ instances along with other administrative definitions
  are not shown here: this is a simple README and not intended to be a literate
  Haskell program, and the actual implementation of this mywork-example example
  can be found in the ~samples/mywork~ directory of this repository).
  #+end_quote

Since this is a TUI application, we will now design the overall appearance of the
application:

#+begin_example
  +---------------------------- mywork-example v0.1 --------------------------+
  | Projects: 30 (Author=8, Contributor=19, User=2), 2017-08-28 to 2022-09-10 |
  |---------------------------------------------------------------------------|
  | Project    | Location (for currently selected project in list)       Date |
  | List       | Location                                                Date |
  |            |   :                                                       :  |
  |            |                                                              |
  |            |                                                              |
  |            |                                                              |
  |            |--------------------------------------------------------------|
  |            | Date - Note for currently selected location                  |
  |            |  :       :                                                   |
  |            |                                                              |
  |            | [this is only visible if a Location is selected]             |
  | search: XX |                                                              |
  |---------------------------------------------------------------------------|
  |  F1 - Load/Save    F2 - Add Project   F3 - Add Location   F4 - Add Note   |
  +---------------------------------------------------------------------------+
#+end_example


There will be a summary line across the top and a list of projects on the left
side.  Location information for the currently selected project in the list will
be shown on the right side, and notes for a location will be shown if the
location is highlighted.  The bottom will show function keys that can be used to
perform activities.  Each of these areas will be a Pane.

Activities:

 * It should be possible to move the cursor between the Project List Pane and the
   Location Pane via the Tab/Shift-Tab key; none of the other areas are
   focusable.

 * Typing when the Project List Pane is focused will modify the "search"
   selection and the visible entries in the list.

 * The function keys are global (they do not depend on which Pane is focused),
   although they may be disabled (and visually marked differently) if not
   applicable in the current mode.

 * The Load/Save operation will bring up a modal dialog window, as will the Add
   Project operation.  Being modal, both of these hold focus until dismissed.

 * And finally, Ctrl-Q will quit the application in any state, and ESC will exit
   from any current dialog, or if there is no dialog, ESC will exit the
   application.

Given the above core data structures, visual depiction, and general
functionality, we can start to use the brick-panes library to build up this
application in stages.

** Startup and Configuration

Our application will need to perform some general initialization at startup time
to declare the Brick environment.  This includes initializing global state.
Since the Panes will each internalize their own state management, the global
state only needs to maintain elements that are globally necessary.  For our
application, this will be the name of the project JSON file, the current Project
data, and the Brick focus ring.  This could be passed on the command line or read
from various configuration sources, but for this simple introduction, it will
just start out with a hard-coded name (although this might change later due to
the Load operation).

  #+begin_src haskell
  data MyWorkCore = MyWorkCore { projFile :: FilePath
                               , myProjects :: Projects
                               , myWorkFocus :: FocusRing WName
                               }

  initMyWorkCore = MyWorkCore { projFile = "projects.json"
                              , myProjects = Projects mempty
                              , myWorkFocus = focusRing [ WProjList, WLocation ]
                              }
  #+end_src


The ~name~ parameter for the Brick ~Widget~ instances will be handled by a simple
declaration:

  #+begin_src haskell
  data WName = WSummary | WProjList | WLocation | WNotes | WOps | WLoader
  #+end_src

For this simple application, there is no application-specific event type.  This
could be specified as ~()~ directly, but we will use a convenient type synonym to
differentiate supplying this type for Brick Event types v.s. other types:

  #+begin_src haskell
  type MyWorkEvent = ()  -- No app-specific event for this simple app
  #+end_src

Each Pane will be identified by its own identifying datatype which will provide
an instance of the ~Pane~ class.  The ~Pane~ class is defined in brick-panes:

  #+begin_src haskell
  class Pane n appEv pane | pane -> n where
      ...
  #+end_src

where the ~n~ parameter is the same type that the application will provide to
Brick's ~Widget~ types.

Note each Pane will need a distinguishing ~Type~.  If there is already a ~Type~
that is a reasonable representation of the data in the Pane, that type can be
used, otherwise a plain data type can be created, as is the instance here for the
summary and operations panes.  We'll start by creating a couple of the primary
panes, and then come back later to add the additional panes.

  #+begin_src haskell
  {-# LANGUAGE MultiParamTypeClasses #-}

  data SummaryPane
  data OperationsPane

  instance Pane WName MyWorkEvent SummaryPane where ...
  instance Pane WName MyWorkEvent Projects where ...
  instance Pane WName MyWorkEvent OperationsPane where ...
  #+end_src

The other types for the instance and the actual instance details will be defined
later.  It's also worth noting that it can be convenient to define each Pane in
its own module file; when done in this manner, the Pane's data type is the only
thing that needs to be exported from the module (if defined in that module).

This core state will be wrapped by the brick-pane ~Panel~ object, which collects
the various ~Pane~ instances, and the result is provided to Brick to initialize
the application.  Here's a summary of the brick-panes definitions for a ~Panel~.

  #+begin_src haskell
  data Panel n appEv state (panes :: [Type]) where ...

  basePanel :: state -> Panel n appev state '[]
  basePanel = ...

  addToPanel :: Pane n appev pane u
                ...
             => PaneFocus n
             -> Panel n appev state panes
             -> Panel n appev state (pane ': panes)
  addToPanel n pnl = ...

  data PaneFocus n = Always | Never | WhenFocused | WhenFocusedModal
  #+end_src

To initialize our Brick application with the core state and the Panes defined
above:

  #+begin_src haskell
  {-# LANGUAGE DataKinds #-}

  type MyWorkState = Panel WName MyWorkEvent MyWorkCore
                     '[ SummaryPane
                      , Projects
                      , OperationsPane
                      ]

  initialState :: MyWorkState
  initialState = addToPanel Never
                 $ addToPanel WhenFocused
                 $ addToPanel Never
                 $ basePanel initMyWorkCore

  myworkApp :: App MyWorkState MyWorkEvent WName
  myworkApp = App { appDraw = drawMyWork
                  , appChooseCursor = showFirstCursor
                  , appHandleEvent = handleMyWorkEvent
                  , appStartEvent = return ()
                  , appAttrMap = const myattrs
                  }

  myattrs = attrMap defAttr
            [
              (editAttr, white `on` black)
            , (editFocusedAttr, yellow `on` black)
            , (listAttr, defAttr `withStyle` defaultStyleMask)
            , (listSelectedAttr, defAttr `withStyle` bold)
            , (listSelectedFocusedAttr, defAttr `withStyle` reverseVideo)
            ]

  main = defaultMain myworkApp initialState
  #+end_src

In this initialization, we've defined the full type for the application, which
consists of the base (global) type of ~MyWorkCore~, followed by a type-level list
of the panes in the application.  The initialization function does not need to
explicitly reference the type of each Pane, but it should add them in the reverse
order they are specified in the type list (the ~$~ composition is right-to-left,
so the order of the two lists is the same).  When adding each Pane, the parameter
specifies what the focus policy for delivering events to that Pane should be.  In
our application, the ~SummaryPane~ will never receive events, the Projects list
pane will receive events when focused, and the ~OperationsPane~ events will be
handled globally rather than by the ~Pane~ since they should apply in any state,
regardless of the focus.

All that's left is to define the ~drawMyWork~ and ~handleMyWorkEvent~ functions,
as well as filling in the ~instance~ declarations introduced above.

** Drawing

When drawing the application, the normal Brick drawing activities are performed,
but drawing Panes in the Panel can be done very generically:

  #+begin_src haskell
  drawMyWork :: MyWorkState -> [Widget WName]
  drawMyWork mws =
    [
      joinBorders
      $ withBorderStyle unicode
      $ borderWithLabel (str $ " mywork-example " <> showVersion version <> " ")
      $ vBox $ catMaybes
      [
        panelDraw @SummaryPane mws
      , Just hBorder
      , panelDraw @Projects mws
      , Just hBorder
      , panelDraw @OperationsPane mws
      ]
    ]
  #+end_src

This is a very simple function that defers the drawing of each Pane to that Pane
via the ~panelDraw~ function.  The ~panelDraw~ return values are a ~Maybe~ value
where ~Nothing~ indicates that the Pane should not currently be drawn; this will
be used later when we add the modal FileLoader and AddProject panes.

** Event Handling

The event handler is also fairly normal to Brick, except that here again, the
Panel provides a common function to call that will dispatch the event to the
various Panes depending on the current focus target and the individual Pane's
event receptivity that was specified as the argument to the ~addToPanel~
initialization call.

  #+begin_src haskell
  handleMyWorkEvent :: BrickEvent WName MyWorkEvent -> EventM WName MyWorkState ()
  handleMyWorkEvent = \case
    AppEvent _ -> return () -- this app does not use these
    -- Application global actions
    --   * CTRL-q quits
    --   * CTRL-l refreshes vty
    --   * ESC dismisses any modal window
    VtyEvent (Vty.EvKey (Vty.KChar 'q') [Vty.MCtrl])  -> halt
    VtyEvent (Vty.EvKey (Vty.KChar 'l') [Vty.MCtrl])  -> do
      vty <- getVtyHandle
      liftIO $ Vty.refresh vty
    -- Otherwise, allow the Panes in the Panel to handle the event
    ev -> do state0 <- get
             (_,state) <- handleFocusAndPanelEvents myWorkFocusL state0 ev
             put state
  #+end_src

The Panel will need to be able to access the focus ring in the base global state
to determine the current focus.  It will need a Lens to do this, so we will
create a simple lens definition here to accomodate that; the lens accessor for
the field itself can be created through a number of different processes aside
from the manual method used below, and brick-panes supplies the ~onBaseState~
lens to translate from the outer state (defined below) to the base global state.

  #+begin_src haskell
  coreWorkFocusL :: Lens' MyWorkCore (FocusRing WName)
  coreWorkFocusL f c = (\f' -> c { myWorkFocus = f' }) <$> f (myWorkFocus c)

  myWorkFocusL :: Lens' MyWorkState (FocusRing WName)
  myWorkFocusL = onBaseState . coreWorkFocusL
  #+end_src

It is useful to observe that the ~handleMyWorkEvent~ handler did not need to
define handlers for ~Tab~/~Shift-Tab~ to switch between panes: the Pane's
~handleFocusAndPanelEvents~ handles these events automatically.


** Initial Panes

At this point, all the general application code is ready to go.  More will be
added later, but now it's time to turn our attention to the individual Panes.

*** Summary Pane

Previously we introduced the need for an ~instance Pane~ for each Pane, including
this ~SummaryPane~, but no instance details were provided.  Here, the brick-panes
~Pane~ class will be developed in more detail in parallel with the
~SummaryPane~'s instance.

**** Initialization

To begin with, it will be necessary to allow the Pane to have internal state, and
to initialize that internal state.  The ~Pane~ class supports this via a data
family declaration and an ~initPaneState~ method as defined in brick-panes:

  #+begin_src haskell
  class Pane n appEv pane | pane -> n where
    data (PaneState pane appEv)  -- State information associated with this Pane
    type (InitConstraints pane initctxt) :: Constraint
    initPaneState :: (InitConstraints pane i) => i -> PaneState pane appEv

    type (InitConstraints pane initctxt) = ()
  #+end_src

An `InitConstraints~ constraint is attached to the ~initPaneState~ method, and
that constraint is defined as part of the Pane instance.  This allows the ~Pane~
instance to specify any constraints that are needed to accomodate actions that
will be performed in the ~initPaneState~ method.  By default, there are no
~InitConstraints~.

At this point, you might recall that the initialization of the Panel was
performed by calls to ~addPanel~, which only passed information about whether
events should be delivered to the state, but there was nothing providing the ~i~
argument that is defined here for the ~initPaneState~ method.  That's because the
~Pane~ class is defined in a very general fashion, but when the ~Pane~ is used as
part of a ~Panel~, the ~i~ parameter defaults to the sub-type of the Panel that
has already been initialized.  This means that for the ~SummaryPane~
initialization call, the ~i~ parameter will be:

  #+begin_src haskell
  Panel WName MyWorkEvent MyWorkCore '[ Projects, OperationsPane ]
  #+end_src

Recall that this is the same as MyWorkState except it is missing the SummaryPanel
entry in the type list.  When initializing the ~Projects~ pane, then the type
will contain only the ~OperationsPane~, and the ~OperationsPane~ initialization
will have access only to the base ~MyWorkCore~ type information.  This heirarchy
of availability may affect the order in which the Panes should be specified in
the top-level type if some Panes will need access to information from other
Panes.  This will be explored in more detail below, but at the present moment,
the ~SummaryPane~ will have no internal state, so it will not need any
~InitConstraints~ defined:

  #+begin_src haskell
  {-# LANGUAGE TypeFamilies #-}
  {-# LANGUAGE TypeSynonymInstances #-}

  instance Pane WName MyWorkEvent SummaryPane where
    data (PaneState SummaryPane MyWorkEvent) = Unused
    initPaneState _ = Unused
  #+end_src

**** Drawing

To draw the pane, the ~Pane~ class provides another method, along with a
corresponding constraint that can be used to encode any necessities for the draw
implementation (which again default to ~()~ representing no constraints).

  #+begin_src haskell
  class Pane n appEv pane | pane -> n where
    data (PaneState pane appEv)  -- State information associated with this Pane
    type (InitConstraints pane initctxt) :: Constraint
    type (DrawConstraints pane drwctxt n) :: Constraint
    initPaneState :: (InitConstraints pane i) => i -> PaneState pane appEv
    drawPane :: (DrawConstraints pane drawcontext n, Eq n)
             => PaneState pane appEv -> drawcontext -> Maybe (Widget n)

    type (InitConstraints pane initctxt) = ()
    type (DrawConstraints pane drwctxt n) = ()
  #+end_src

The ~drawPane~ method takes two arguments and returns a ~Maybe~.  As discussed
earlier in the general application drawing section, a ~Pane~ can return ~Nothing~
to indicate it shouldn't be drawn at the present time.  The ~SummaryPane~ is
always drawn, so it will always return a ~Just~ value.

The first argument provided to the ~drawPane~ method is the data family value
defined for this pane and initialized by the ~initPaneState~.

The second parameter is an abstract context for drawing.  As with the
~initPaneState~ method, the ~Pane~ class defines this in a very generic manner,
but when the ~Pane~ is used in a ~Panel~, the ~Panel~ provides the sub-state of
the ~Panel~ that *includes* the current ~Pane~, but not the elements preceeding
it in the type list.  Here, the ~SummaryPane~ is the first element in the
~MyWorkState~, so its ~drawPane~ will receive the full ~MyDrawState~ value, but
the panes beneath it will receive subsequently lesser sub-type portions.

For the ~SummaryPane~, the ~drawPane~ instance will need to display the number of
~Projects~ sub-divided by the ~Project~ ~Role~, as well as the full date range
for all ~Projects~.  To obtain this information, it will need access to the
~Projects~ data that is contained in the global base state ~MyWorkCore~.  To
obtain this information, it needs to translate the ~drawcontext~ argument to the
~Projects~ list contained in the base global state; it can indicate this need via
the ~DrawContext~ as follows:

  #+begin_src haskell
  instance Pane WName MyWorkEvent SummaryPane where
    data (PaneState SummaryPane MyWorkEvent) = Unused
    type (DrawConstraints SummaryPane s WName) = ( HasProjects s )
    initPaneState _ = Unused
    drawPane _ s = Just $ drawSummary (getProjects s)

  drawSummary :: Projects -> Widget WName
  drawSummary prjs = ...
  #+end_src

The ~HasProjects~ constraint is a class that our application will defined as
capable of providing the ~getProjects~ method.  The instance of that class for
the global base ~MyWorkCore~ object is simple, and the instance of that class
for a ~Panel~ wrapper of that global base state can use the ~onBaseState~ lens
previously discussed:

  #+begin_src haskell
  class HasProjects s where
    getProjects :: s -> Projects

  instance HasProjects MyWorkCore where
    getProjects = myProjects

  instance HasProjects (Panel WName MyWorkEvent MyWorkCore panes) where
    getProjects = getProjects . view onBaseState
  #+end_src

Now all that's needed is the body of the ~drawSummary~ function itself:

  #+begin_src haskell
  drawSummary :: Projects -> Widget WName
  drawSummary prjcts =
    let prjs = projects prjcts
        prjcnt = str $ "# Projects=" <> show (length prjs) <> subcounts
        subcounts = (" (" <>)
                    $ (<> ")")
                    $ List.intercalate ", "
                    [ show r <> "=" <> show (length fp)
                    | r <- [minBound .. maxBound]
                    , let fp = filter (isRole r) prjs
                    , not (null fp)
                    ]
        isRole r p = r == role p
        dateRange = str (show (minimum projDates)
                         <> ".."
                         <> show (maximum projDates)
                        )
        locDates prj = catMaybes (locatedOn <$> locations prj)
        projDates = concatMap locDates prjs
    in vLimit 1
       $ if null prjs
         then str "No projects defined"
         else prjcnt <+> fill ' ' <+> dateRange
  #+end_src

Note that all of the complexity of this drawing functionality, as well as
determining the arguments to it are internal to the Pane implementation (usually
in its own file) and supporting classes and instances; the top-level draw
operation retains its simplicity.

**** Summary Pane Notes

Since the Summary pane does not have internal state to be updated and it does not
handle events, the above is sufficient to fully define the ~SummaryPane~!


*** Project List Pane

Now that the ~SummaryPane~ has been implemented, we turn our attention to the
Project List Pane.  This pane will also need access to the list of Projects, but
it can re-use the previously defined ~HasProjects~ class in its constraints where
necessary.

**** Initialization

This Pane is slightly more complex: it will contain a ~Brick.Widgets.List~ and
also a ~Brick.Widgets.Edit~ to handle the search filter.  There are two choices
here: create the ~Brick.Widgets.List~ widget as part of the long-term ~Pane~
state, or dynamically create the ~Brick.Widgets.List~ widget each time it is
drawn.  The former choice is better, since the ~Brick.Widgets.List~ will then
automatically maintain its own internal state such as the currently selected
item, etc.  Thus, the ~Pane~ state will need to contain these two Brick widgets
and the initialization method should prepare them.

  #+begin_src haskell
  instance Pane WName MyWorkEvent Projects where
    data (PaneState Projects MyWorkEvent) = P { pL :: List WName Text
                                              , pS :: Editor Text WName
                                              }
    type (InitConstraints Projects s) = ( HasProjects s )
    initPaneState s = let prjs = projects $ getProjects s
                          pl = list WPList (Vector.fromList (name <$> prjs)) 1
                          ps = editor WPFilter (Just 1) ""
                      in P pl ps
  #+end_src

Note that both the List and the Editor widgets require a unique ~WName~ value.
These values should also be added to the global ~WName~ definition previously
introduced above.

This is also a good demonstration of the encapsulation that the brick-panes
library provides: the primary application simply needs the ability to display and
allow selection of a project.  The actual details of how the display is performed
and how the selection is performed is not visible or important outside of the
implementation of the ~Pane~.

**** Drawing

Drawing this pane is relatively simple and primarily just invokes the draw for
the two Widgets it contains.

  #+begin_src haskell
  instance Pane WName MyWorkEvent Projects where
    data (PaneState Projects MyWorkEvent) = P { pL :: List WName Text
                                              , pS :: Editor Text WName
                                              }
    type (InitConstraints Projects s) = ( HasProjects s )
    type (DrawConstraints Projects s WName) = ( HasFocus s WName )
    initPaneState s = let prjs = projects $ getProjects s
                          pl = list WPList (Vector.fromList (name <$> prjs)) 1
                          ps = editor WPFilter (Just 1) ""
                      in P pl ps
    drawPane ps gs =
      let isFcsd = gs^.getFocus.to focused == Just WProjList
          lst = renderList (const txt) isFcsd (pL ps)
          srch = str "Search: " <+> renderEditor (txt . head) isFcsd (pS ps)
      in Just $ vBox [ lst, fill ' ', srch ]
  #+end_src

Unlike the ~SummaryPane~, this pane's draw code does not necessarily access to
the global base state, but it does need access to the FocusRing in order to tell
the List renderer if the list has focus.  This can be done by defining another
class ~HasFocus~ that will be similar to the ~HasProjects~ class described above;
since this is a very common need, the brick-panes library already provides this
class (with a ~getFocus~ lens method) and a Panel instance for it, so all that is
needed here is the instance definition to extract the FocusRing from the global
base state.

  #+begin_src haskell
  instance HasFocus MyWorkCore WName where
    getFocus f s =
      let setFocus jn = case focused jn of
            Nothing -> s
            Just n -> s & coreWorkFocusL %~ focusSetCurrent n
      in setFocus <$> (f $ Focused $ focusGetCurrent (s^.coreWorkFocusL))
  #+end_src

One thing to note about the draw implementation above is that the focused
indication passed to both the list and edit widgets is *not* based on their
individual ~WName~ values but instead on the ~WName~ of the ~Projects Pane~
itself.  This is because the pane will receive focus and will direct events to
both widgets (which conveniently do not overlap in their event handling).  There
is no specific additional differentiation or selectability between the list and
edit widgets.

**** Event Handling

As with the initialization and the drawing Pane operations, there is an operation
and corresponding constraint defined by brick-panes for allowing the ~Pane~ to
handle events:

  #+begin_src haskell
  class Pane n appEv pane | pane -> n where
    data (PaneState pane appEv)   -- State information associated with this pane
    type (InitConstraints pane initctxt) :: Constraint
    type (DrawConstraints pane drwctxt n) :: Constraint
    type (EventConstraints pane evctxt) :: Constraint
    type (EventType pane n appEv)
    initPaneState :: (InitConstraints pane i) => i -> PaneState pane appEv
    drawPane :: (DrawConstraints pane drawcontext n, Eq n)
             => PaneState pane appEv -> drawcontext -> Maybe (Widget n)
    focusable :: (EventConstraints pane eventcontext, Eq n)
              => eventcontext -> PaneState pane appEv -> Seq.Seq n
    handlePaneEvent :: (EventConstraints pane eventcontext, Eq n)
                    => eventcontext
                    -> EventType pane n appEv
                    -> PaneState pane appEv
                    -> EventM n es (PaneState pane appEv)
    updatePane :: UpdateType pane -> PaneState pane appEv -> PaneState pane appEv

    -- A set of defaults that allows a minimal instance specification
    type (InitConstraints pane initctxt) = ()
    type (DrawConstraints pane drwctxt n) = ()
    type (EventConstraints pane evctxt) = ()
    type (EventType pane n appev) = Vty.Event  -- by default, handle Vty events
    focusable _ _ = mempty
    handlePaneEvent _ _ = return
    type (UpdateType pane) = ()
    updatePane _ = id
  #+end_src

The additional element involved in handling events is the ~EventType~ type family
declaration above, which can be used to specify which type of Event the Pane will
respond to.  Brick Events are arranged in a heirarchy of relationships, where the
higher level event can handle Mouse events and application-level as well as
Keyboard events, and the ~EventType~ can be set to indicate which type of event
this ~Pane~ should be provided with (where the default is Keyboard events).  The
~Panel~'s ~handleFocusAndPanelEvents~ will automatically pass the correct
~EventType~ to the ~Pane~ ~handlePaneEvent~ method.

There is also a new ~focusable~ method in the ~Pane~ class, which is used to
determine if any Widgets that are part of the Pane can be members of the
FocusRing at the current time.  This is used by the ~Panel~ after processing each
event to determine the new ~FocusRing~ contents.  This is frequently used in
concert with returning ~Nothing~ from the ~drawPane~ method, but it is
independent and allows for potentially multiple Widgets to be focusable.  Since
the Projects Pane is always focusable, it will return its own ~WName~ value as
the single response.

Similar to drawing then, event handling for the ~Projects~ ~Pane~ consists of
simply passing the event to the underlying widgets.  As noted above, passing the
same event to multiple widgets could cause confusion, but in this case the only
common events are the arrow events, and since the edit widget height is 1 it
should ignore the vertical arrows that will be used to navigate the list entries.
The ~handleEditorEvent~ called internally expects a ~BrickEvent~, so the
~EventType~ must be specified accordingly.  And finally, a couple of helper
lenses are defined:

  #+begin_src haskell
  instance Pane WName MyWorkEvent Projects where
    data (PaneState Projects MyWorkEvent) = P { pL :: List WName Text
                                              , pS :: Editor Text WName
                                              }
    type (InitConstraints Projects s) = ( HasProjects s )
    type (DrawConstraints Projects s WName) = ( HasFocus s WName )
    type (EventType Projects WName MyWorkEvent) = BrickEvent WName MyWorkEvent
    initPaneState s = let prjs = projects $ getProjects s
                          pl = list WPList (Vector.fromList (name <$> prjs)) 1
                          ps = editor WPFilter (Just 1) ""
                      in P pl ps
    drawPane ps gs =
      let isFcsd = gs^.getFocus.to focused == Just WProjList
          lst = renderList (const txt) isFcsd (pL ps)
          srch = str "Search: " <+> renderEditor (txt . head) isFcsd (pS ps)
      in Just $ vBox [ lst, fill ' ', srch ]
    handlePaneEvent _ ev ps =
      do ps1 <- case ev of
                  VtyEvent ev' -> do
                    r <- nestEventM' (pL ps) (handleListEvent ev')
                    return $ ps & pList .~ r
                  _ -> return ps
         srch <- nestEventM' (ps ^. pSrch) (handleEditorEvent ev)
         return $ ps1 & pSrch .~ srch
    focusable _ _ = Seq.singleton WProjList


  pList :: Lens' (PaneState Projects MyWorkEvent) (List WName Text)
  pList f ps = (\n -> ps { pL = n }) <$> f (pL ps)

  pSrch :: Lens' (PaneState Projects MyWorkEvent) (Editor Text WName)
  pSrch f ps = (\n -> ps { pS = n }) <$> f (pS ps)
  #+end_src


**** Project List Pane Notes

At this point, the Project List pane is now fully defined.  In addition, the
~Pane~ class is /almost/ fully described: there will only be one more member that
will be introduced later in this development description.


*** Operations Pane

The Operations Pane specifies the operations that can be performed and the key
sequences that initiate them.  This Pane does not itself take focus: the key
bindings are application global.  It may be however that certain key bindings are
inactive in the current mode.

**** Initialization

This Pane stores no internal data, so no internal storage or initialization is
needed.

  #+begin_src haskell
  instance Pane WName MyWorkEvent OperationsPane where
    data (PaneState OperationsPane MyWorkEvent) = Unused
    initPaneState _ = Unused
  #+end_src

**** Drawing

This Pane is drawn with the ability to adjust the presented operations to
indicate if they are active or not.  It must therefore have a class constraint
that can indicate the active state for those bindings:

  #+begin_src haskell
  class HasSelection s where
    selectedProject :: s -> Maybe Project
  #+end_src

The main instance for this will be for the Project List pane's state:

  #+begin_src haskell
  {-# LANGUAGE FlexibleInstances #-}

  instance HasSelection (PaneState Projects MyWorkEvent) where
    selectedProject = fmap snd . listSelectedElement . pL
  #+end_src

That pane state is not generally available outside the implementation for that
pane however, so how will this information be available to the Operations Pane?
The brick-panes library provides an `onPane` lens that can access a particular
Pane's state from anywhere "above" that Pane in the Panel type list, provided
that the `PanelOps` constraint can be satisfied.  This can be used to define a
`HasSelection` instance that will work for the Panel.

  #+begin_src haskell
  instance ( PanelOps Projects WName MyWorkEvent panes MyWorkCore
           , HasSelection (PaneState Projects MyWorkEvent)
           )
    => HasSelection (Panel WName MyWorkEvent MyWorkCore panes) where
    selectedProject = selectedProject . view (onPane @Projects)
  #+end_src

However, the first attempt to build with this will receive the following error:

  #+begin_example
  samples/mywork/Main.hs:67:18: error:
    • No Projects in Panel
      Add this pane to your Panel (or move it lower)
      (Possibly driven by DrawConstraints)
    ...
  #+end_example

This indicates that the Projects Pane is /above/ the Operations Pane, so the
latter cannot satisfy the `HasSelection` instance.  To fix this, simply re-order
the type list for the main state and the initialization operation:

  #+begin_src haskell
  type MyWorkState = Panel WName MyWorkEvent MyWorkCore
                     '[ SummaryPane
                      , OperationsPane
                      , Projects
                      ]

  initialState :: MyWorkState
  initialState = addToPanel Never
                 $ addToPanel Never
                 $ addToPanel WhenFocused
                 $ basePanel initMyWorkCore
  #+end_src

By "stacking" Panes in the right order in the Panel, most cross-pane dependencies
can be satisfied.  If there are cases where a total ordering is not possible,
then state maintained by a Pane may need to be moved into the global base state
to break the dependency cycle.

Now that the `HasSelection` is defined to determine if a Project is currently
selected, the draw functionality for the Operations pane can be made sensitive to
that setting.

  #+begin_src haskell
  instance Pane WName MyWorkEvent OperationsPane where
    data (PaneState OperationsPane MyWorkEvent) = Unused
    type (DrawConstraints OperationsPane s WName) = ( HasSelection s )
    initPaneState _ = Unused
    drawPane _ gs =
      let projInd = case selectedProject gs of
                      Nothing -> withAttr (attrName "disabled")
                      Just _ -> id
          ops = List.intersperse (fill ' ')
                [ str "F1-Load/Save"
                , str "F2-Add Project"
                , projInd $ str "F3-Add Location"
                , projInd $ str "F4-Add Note"
                ]
      in Just $ vLimit 1 $ str " " <+> hBox ops <+> str " "
  #+end_src

And the final change is to add the following to the ~myattrs~ map:

  #+begin_src haskell
  ...
  , (attrName "disabled", defAttr `withStyle` dim)
  ...
  #+end_src

**** Event Handling

The ~OperationsPane~ does not directly handle events: all key bindings it
describes are handled by global event handling, which will be added later.  The
`OperationsPane` is now fully defined and no more is needed at the moment.

*** Adding the Location Pane

The next step in the design of the application is to add the ~Location~ Pane,
which wasn't previously defined.  We'll need to add the Pane to the global Panel
type and initialization:

  #+begin_src haskell
  type MyWorkState = Panel WName MyWorkEvent MyWorkCore
                     '[ SummaryPane
                      , OperationsPane
                      , Location
                      , Projects
                      ]

  initialState :: MyWorkState
  initialState = focusRingUpdate myWorkFocusL
                 $ addToPanel Never
                 $ addToPanel Never
                 $ addToPanel WhenFocused
                 $ addToPanel WhenFocused
                 $ basePanel initMyWorkCore

  #+end_src

The ~Location~ Pane was added "above" the ~Projects~ pane, because it will need
to show the Location for the currently selected Pane, which it will need to
retrieve via the ~HasSelection~ constraint in the same manner as the
~OperationsPane~.

In addition, there is a new ~focusRingUpdate~ function called to modify the
initial state.  This function is provided by brick-panes and its responsibility
is updating the ~FocusRing~ based on the current set of focusable Panes.  Here,
this adds the ~Location~ and ~Projects~ panes to the focusable list.  The
~focusRingUpdate~ function should also be called whenever something happens that
would modify the focus ring (e.g. a modal...).

Rather than showing how each aspect of the ~Location~ Pane is defined, the whole
thing is presented here at once:

  #+begin_src haskell
  instance Pane WName MyWorkEvent Location where
    data (PaneState Location MyWorkEvent) = L { lL :: List WName (Text, Maybe Day) }
    type (InitConstraints Location s) = ( HasSelection s, HasProjects s )
    type (DrawConstraints Location s WName) = ( HasFocus s WName, HasSelection s )
    initPaneState gs =
      let l = L (list WLList mempty 2)
          update x = do p <- selectedProject gs
                        prj <- DL.find ((== p) . name) (projects $ getProjects gs)
                        return $ updatePane prj x
      in fromMaybe l $ update l
    drawPane ps gs =
      let isFcsd = gs^.getFocus.to focused == Just WLocation
          rndr (l,d) = (txt l
                        <+> hFill ' '
                        <+> (str $ maybe "*" show d)
                       )
                       <=> str " "
      in Just $ renderList (const rndr) isFcsd (lL ps)
    focusable _ ps = focus1If WLocation $ not $ null $ listElements $ lL ps
    handlePaneEvent _ ev ps = do r <- nestEventM' (lL ps) (handleListEvent ev)
                                 return $ ps & lList .~ r
    type (UpdateType Location) = Project
    updatePane prj ps =
      let ents = [ (location l, locatedOn l) | l <- locations prj ]
      in L $ listReplace (V.fromList ents) (Just 0) (lL ps)

  lList :: Lens' (PaneState Location MyWorkEvent) (List WName (Text, Maybe Day))
  lList f ps = (\n -> ps { lL = n }) <$> f (lL ps)
  #+end_src

In the above, the final method for the ~Pane~ is introduced: the ~updatePane~
method, along with the ~UpdateType~ specification (which previously defaulted to
~()~).  The ~UpdateType~ specifies the type of the value passed to the
~updatePane~ method's first argument.  This method is called externally with the
specified argument whenever the Pane's internal state should be updated.  Here,
it is intended to be called with the ~Project~ for which the ~Location~ pane
should show the locations, and it will update the internal ~Brick.Widges.List~
with those locations.  This is also called directly from the ~initPaneState~ when
there is a selection at initialization time.

Also of interest is the new ~focus1If~ function called by the ~focusable~ method.
This brick-panes function is a convenience helper that returns the first argument
in a single-entry Sequence if the second argument is true.  The automatic call of
~focusRingUpdate~ performed internally by the ~Panel~ at the end of handling each
event will use the return values of the ~focusable~ methods to update the
~FocusRing~ appropriately.  The ~focus1If~ helper is being used to indicate that
the ~Location~ Pane should not receive focus unless there are actual locations
being displayed.


Note that a ~WLList~ value was added to the ~WName~ type as well, and the main
~drawMyWork~ is updated to draw the Location pane:

  #+begin_src haskell
  drawMyWork mws =
    [
      joinBorders
      $ withBorderStyle unicode
      $ borderWithLabel  (str $ " mywork-example " <> showVersion version <> " ")
      $ vBox $ catMaybes
      [
        panelDraw @SummaryPane mws
      , Just hBorder
      , Just $ hBox $ catMaybes
        [ hLimitPercent 20 <$> panelDraw @Projects mws
        , Just vBorder
        , panelDraw @Location mws
        ]
      , Just hBorder
      , panelDraw @OperationsPane mws
      ]
    ]
  #+end_src

The ~Location~ Pane's ~updatePane~ should be called whenever the ~Projects~ Pane
selection is changed, to update the Locations displayed.  This is handled by
extending the application's primary event handler to detect these changes and
explicitly call the ~updatePane~ as seen in the modified excerpt here:

  #+begin_src haskell
    ...
    -- Otherwise, allow the Panes in the Panel to handle the event
    ev -> do state0 <- get
             let proj0 = selectedProject state0
             (_,state) <- handleFocusAndPanelEvents myWorkFocusL state0 ev
             let mprj = do pnm <- selectedProject state
                           guard (Just pnm /= proj0)
                           Data.List.find ((== pnm) . name)
                                          (projects $ getProjects state)
             let state' =
                   case mprj of
                     Just p -> state & onPane @Location %~ updatePane p
                     _ -> state
             put state'
  #+end_src


**** Location Pane Notes

At this point, the development of the application is progressing nicely.  Each
additional Pane is defined with its own isolated specification, information
exchanged with other Panes is explicit and controlled by the Constraints, and
global application changes needed are just to ensure that the Pane is added to
the initialization operations and type, ensure it is part of the drawing code,
and add any /special/ event handling needed for that Pane.

Most of the rest of the development of the mywork-example application will follow
this pattern, but it's worth looking at one additional aspect: modal panes.


*** File Load/Save Pane

The File Load/Save (a.k.a. ~FileMgr~) Pane is somewhat different from the
previous panes in that it is a modal pane: it is invisible until activated, and
while activated it holds the focus until de-activated.

The design and appearance of the ~FileMgr~ Pane will be a centered modal window,
displaying a Brick ~FileBrowser~ Widget at the top, help information below that,
and a ~Save~ button at the bottom.

The ~Save~ button will be selectable via the
~Tab~/~Shift-Tab~ events, and hitting ~Space~ or ~Return~ while the button is
selected will perform the save action on the to the currently selected file in
the file browser.

When the ~FileBrowser~ Widget is selected, normal browsing can be performed, and
~Return~ will load the currently selected file and dismiss the ~FileMgr~ modal
pane, whereas ~ESC~ at any point will dismiss the ~FileMgr~ modal pane without
making any changes.

# Support for a modal Pane such as this mainly requires a flag in the global base state indicating whether the pane is active or not.

**** Pane Implementation

The FileMgr Pane itself is implemented in the manner we have come to expect,
although there are a couple of adjustments:

  #+begin_src haskell
  data FileMgrPane

  instance Pane WName MyWorkEvent FileMgrPane where
    data (PaneState FileMgrPane MyWorkEvent) =
      FB { fB :: Maybe (FileBrowser WName)
           -- ^ A Nothing value indicates the modal is not currently active
         , myProjects :: Projects
           -- ^ Current loaded set of projects
         , newProjects :: Bool
           -- ^ True when myProjects has been updated; clear this via updatePane
         }
    type (InitConstraints FileMgrPane s) = ()
    type (DrawConstraints FileMgrPane s WName) = ( HasFocus s WName )
    type (EventConstraints FileMgrPane e) = ( HasFocus e WName )
    initPaneState gs = FB Nothing (Projects mempty) False
    drawPane ps gs = drawFB gs <$> fB ps
    focusable _ ps = case fB ps of
                       Nothing -> mempty
                       Just _ -> Seq.fromList [ WFBrowser, WFSaveBtn ]
    handlePaneEvent bs ev ts =
      let isSearching = maybe False fileBrowserIsSearching (ts^.fBrowser)
      in case ev of
        Vty.EvKey Vty.KEsc [] | not isSearching -> return $ ts & fBrowser .~ Nothing
        _ -> case bs^.getFocus of
               Focused (Just WFBrowser) -> handleFileLoadEvent ev ts
               Focused (Just WFSaveBtn) -> handleFileSaveEvent ev ts
               _ -> return ts
    type (UpdateType FileMgrPane) = Bool
    updatePane newFlag ps = ps { newProjects = newFlag }

  fBrowser :: Lens' (PaneState FileMgrPane MyWorkEvent) (Maybe (FileBrowser WName))
  fBrowser f ps = (\n -> ps { fB = n }) <$> f (fB ps)

  myProjectsL :: Lens' (PaneState FileMgrPane MyWorkEvent) Projects
  myProjectsL f wc = (\n -> wc { myProjects = n }) <$> f (myProjects wc)
  #+end_src

  The first observation is that the actual ~Projects~ list is moved here from the
  global base state.  This is to allow the ~FileMgr~ to easily access and replace
  the ~Projects~ data when a file is loaded or saved.

  There is also a flag that indicates when the ~Projects~ has been changed.  This
  will be needed to inform the ~Projects~ Pane that it needs to update its list
  values.  The flag is set internally when a new set of Projects is loaded, and
  the ~updatePane~ can be called to clear the flag once the ~Projects~ Pane has
  been updated.

  The ~focusable~ is also modified to return a list of the two sub-widgets.  This
  is to support the automatic selection of active widget via the
  ~Tab~/~Shift-Tab~ event handling provided by the ~Panel~ implementation.  (The
  ~WName~ datatype is extended in the obvious manner with these new
  constructors.)

  To support the export of the new ~newProjects~ flag, the ~HasProjects~ class is
  slighly updated, and provide an instance for this Pane and any super-Pane
  types, but not for the base global state.

  #+begin_src haskell
  class HasProjects s where
    getProjects :: s -> (Bool, Projects)

  instance ( PanelOps FileMgrPane WName MyWorkEvent panes MyWorkCore
           , HasProjects (PaneState FileMgrPane MyWorkEvent)
           )
    => HasProjects (Panel WName MyWorkEvent MyWorkCore panes) where
    getProjects = getProjects . view (onPane @FileMgrPane)

  instance HasProjects (PaneState FileMgrPane MyWorkEvent) where
    getProjects ps = (newProjects ps, myProjects ps)
  #+end_src

  Various miscellaneous and obvious adjustments will need to be made to
  accomodate the change in return value; these are not shown here.

  The application type and initialization are updated to include the new Pane,
  with the indication that the pane should receive ~Events~ only when
  modally-active:

  #+begin_src haskell
  type MyWorkState = Panel WName MyWorkEvent MyWorkCore
                     '[ SummaryPane
                      , OperationsPane
                      , Location
                      , Projects
                      , FileMgrPane
                      ]

  initialState :: MyWorkState
  initialState = focusRingUpdate myWorkFocusL
                 $ addToPanel Never
                 $ addToPanel Never
                 $ addToPanel WhenFocused
                 $ addToPanel WhenFocused
                 $ addToPanel WhenFocusedModal
                 $ basePanel initMyWorkCore
  #+end_src

  The drawing and handling functions are also not shown here; their
  implementation is relatively straightforward and doesn't reveal any new
  brick-pane concepts.  When a file is actually loaded, the handler will update
  the ~myProjects~ field with the loaded data and set the ~newProjects~ to
  ~True~.

  Of note is the initialization: the Brick ~FileBrowser~ initialization must be
  performed in the ~IO~ monad.  Conveniently, this ~Pane~ is modal and not
  displayed by default, so there is an ~Event~ which causes it to be displayed
  and which can provide the monadic context for the initialization in the global
  event handler:

  #+begin_src haskell
    ...
    VtyEvent (Vty.EvKey (Vty.KFun 1) []) -> do
      fmgr <- liftIO initFileMgr
      modify ((focusRingUpdate myWorkFocusL) . (onPane @FileMgrPane .~ fmgr))
    -- Otherwise, allow the Panes in the Panel to handle the event
    ev -> do state0 <- get
             ...
  #+end_src

  Note here the call to ~focusRingUpdate~: the ~Panel~ event handler
  automatically calls this, but that handler is not used in this situation, so
  the ~FocusRing~ should be explicitly updated with this function.  If this
  update is omitted, the modal will not visibly show the focused state until the
  /next/ event (that calls the ~Panel~'s event handler) is processed.

  In the ~FileMgr~ Pane implementation, the ~initFileMgr~ function is defined:

  #+begin_src haskell
  initFileMgr :: IO (PaneState FileMgrPane MyWorkEvent)
  initFileMgr = do
    fb <- newFileBrowser selectNonDirectories WFBrowser Nothing
    return $ initPaneState fb & fBrowser .~ Just fb
  #+end_src

  Also in the global event handler, the new projects flag is checked, and if it
  is ~True~, it is reset to ~False~ and the ~Projects~ Pane is notified of the
  new ~Projects~ data:

  #+begin_src haskell
    ev -> do proj0 <- gets selectedProject
             s <- get
             (_,s') <- handleFocusAndPanelEvents myWorkFocusL s ev
             put s'
             (new,prjs) <- gets getProjects
             when new $
               modify $ \s -> s
                              & focusRingUpdate myWorkFocusL
                              & onPane @Projects %~ updatePane prjs
                              & onPane @FileMgrPane %~ updatePane False
             ...
  #+end_src

  This invokes the ~Projects~ Pane ~updatePane~ method which is added to support
  updating the displayed projects based on the new data:

  #+begin_src haskell
  instance Pane WName MyWorkEvent Projects where
    ...
    type (UpdateType Projects) = Projects
    updatePane newprjs =
      (pList %~ listReplace (Vector.fromList (name <$> projects newprjs)) (Just 0))
      .
      (pSrch . editContentsL %~ Text.Zipper.clearZipper)
  #+end_src

  There's also an alternative to saving and returning the ~new~ indication from
  ~handleFocusAndPanelEvents~: the transition detection within brick-panes.  In
  the above example, the first element of the tuple returned by
  ~handleFocusAndPanelEvents~ is discarded, but it is a ~PanelTransition~ object.
  There are two brick-panes functions that take a ~PanelTransition~ as an
  argument: ~enteredModal~ and ~exitedModal~.  These can be used to detect if the
  current event handling caused a modal to be newly displayed or dismissed, and
  this can be used to perform various actions.  The following shows the global
  event handler code that might use this method:

  #+begin_src haskell
    ev -> do proj0 <- gets selectedProject
             s <- get
             (trns,s') <- handleFocusAndPanelEvents myWorkFocusL s ev
             put s'
             when (exitedModal @FileMgr trns s') $
               modify $ \s -> s
                              & focusRingUpdate myWorkFocusL
                              & onPane @Projects %~ updatePane (snd $ getProjects s)
                              & onPane @FileMgrPane %~ updatePane False
             ...
  #+end_src

  This implementation is slightly less efficient since it will perform the
  updates on every exit from the FileMgr modal even if there were no changes to
  the Projects it manages, but it demonstrates the usefulness of the
  ~PanelTransition~ indication.  There is also a ~isPanelModal~ function that
  returns True if the Panel is currently showing a Modal pane.

  Finally, the draw function is modified to draw the modal (if drawable) before
  the other Panes, drawing those Panes with the ~"disabled"~ attribute if the
  modal is active.

  #+begin_src
drawMyWork mws =
  let mainPanes =
        [
          borderWithLabel  (str $ " mywork-example " <> showVersion version <> " ")
          $ vBox $ catMaybes
          [
            panelDraw @SummaryPane mws
            ...
          ]
        ]
      allPanes = catMaybes [ panelDraw @FileMgrPane mws ] <> mainPanes
      disableLower = \case
        (m:ls) -> m : (withDefAttr (attrName "disabled") <$> ls)
        o -> o
  in joinBorders . withBorderStyle unicode <$> disableLower allPanes
  #+end_src
  
**** FileMgr Pane Notes

  Not all of the details of the ~FileMgr~ modal Pane implementation are shown
  above, but the remainder is relatively mechanical.  The ~samples/mywork-example~
  directory can be consulted for the more complete implementation details.

** Closing Notes

  At this point, all of the functionality provided by the brick-panes library
  has been introduced, along with examples of code utilizing that functionality.
  We have seen how to add a new Pane, including modal panes, and how to
  coordinate both information sharing and isolation between the various Panes.

  Rather than pedantically walk through the remainder of the creation of the
  ~mywork-example~ application implementation, the completion and extensions of
  this sample application are left as exercises for the reader:

  * Implement the Notes Pane, displaying the Notes associated with the selected
    Location.

  * Implement the Add Project operation

  * Implement the Add Location operation

  * Implement the Add Notes operation

  * Add handling for the Projects Search box, modifying the display of the listed
    Projects based on the entry in the Search box.

  * Add error handling and display (e.g. loading invalid files)

  * Add display of additional Project information (description, language, role,
    etc.).


  If this sample application is intriguing as a potentially useful application
  for daily use, a much more sophisticated and complete version is available from
  Hackage or https://github.com/kquick/mywork.

* FAQ

 * Why not just use Brick Widgets?

   Brick Widgets are a great abstraction, but they are a fairly low-level
   abstraction that don't inherently support multiple, focusable sub-components
   and a generic abstraction interface.