packages feed

brick-panes (empty) → 1.0.0.0

raw patch · 14 files changed

+2918/−0 lines, 14 filesdep +aesondep +basedep +brick

Dependencies added: aeson, base, brick, brick-panes, bytestring, containers, directory, microlens, text, text-zipper, time, vector, vty

Files

+ CHANGELOG.md view
@@ -0,0 +1,28 @@+# Revision history for brick-panes++## 1.0.0.0 -- 2022-10-01++* Public release.++## 0.3.0.0 -- 2022-09-29++* Updated `handleFocusAndPanelEvents` to return a `PanelTransition` indication+  along with the new `Panel`.  This can be used by applications perform+  additional actions if the event just handled caused a transition into or out-of+  a modal Pane.+* The `exitedModal` and `enteredModal` functions can be used to determine the+  type of transition (if any).+* The `isPanelModal` function can be used to determine the current modal state of+  the Panel.++## 0.2.0.0 -- 2022-09-22++* Removed argument from `WhenFocusedModal` and+  `WhenFocusedModalHandlingAllEvents` `PaneFocus` constructors.  These arguments+  should always have been specified as `Nothing` by client code, with deleterious+  behavior if this requirement was not followed, so the need for this awkward+  constant representation was removed.++## 0.1.0.0 -- 2022-09-10++* Initial version.
+ LICENSE view
@@ -0,0 +1,13 @@+Copyright (c) 2022 Kevin Quick++Permission to use, copy, modify, and/or distribute this software for any purpose+with or without fee is hereby granted, provided that the above copyright notice+and this permission notice appear in all copies.++THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS+OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF+THIS SOFTWARE.
+ README.org view
@@ -0,0 +1,1324 @@+* 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.
+ brick-panes.cabal view
@@ -0,0 +1,96 @@+cabal-version:      3.0+name:               brick-panes+version:            1.0.0.0+synopsis:+    Panes library for Brick providing composition and isolation for TUI apps.++description:+   The Panes library is an overlay for Brick that allows individual TUI+   screen areas to be independently developed and then easily composed+   into the overall application.++homepage:           https://github.com/kquick/brick-panes+license:            ISC+license-file:       LICENSE+author:             Kevin Quick+maintainer:         kquick@galois.com+copyright:          (c) Kevin Quick, 2022+category:           Graphics+build-type:         Simple+tested-with:        GHC == 8.8.4, GHC == 8.10.7, GHC == 9.2.4, GHC == 9.4.2++extra-doc-files:    CHANGELOG.md+                  , README.org++source-repository head+    type: git+    location: https://github.com/kquick/brick-panes++flag examples+    description: Build example programs+    default: True+    manual: False++common settings+    ghc-options:  -Wall+                  -Wcompat+                  -Wincomplete-uni-patterns+                  -Wsimplifiable-class-constraints+                  -Wpartial-fields+                  -fprint-potential-instances+                  -fhide-source-paths++library+    import:           settings+    hs-source-dirs:   src+    default-language: Haskell2010+    exposed-modules:  Brick.Panes+    build-depends:    base >= 4.13 && < 4.18+                    , brick >= 1.0 && < 1.4+                    , containers+                    , microlens >= 0.4.11.2 && < 0.5+                    , vty >= 5.35 && < 5.38++executable mywork-example+    import:           settings+    default-language: Haskell2010+    hs-source-dirs:   samples/mywork+    main-is:          Main.hs+    ghc-options:      -threaded+    other-modules:    Defs+                      InitialData+                      Panes.FileMgr+                      Panes.Location+                      Panes.Operations+                      Panes.Projects+                      Panes.Summary+                      Paths_brick_panes+    autogen-modules:  Paths_brick_panes+    build-depends:    base >= 4.13 && < 4.18+                    , brick >= 1.0 && < 1.4+                    , brick-panes+                    , aeson >= 2.0 && < 2.2+                    , bytestring >= 0.10 && < 0.13+                    , containers+                    , directory >= 1.3 && < 1.4+                    , microlens >= 0.4.11.2 && < 0.5+                    , text+                    , text-zipper >= 0.12 && < 0.13+                    , time+                    , vector >= 0.12 && < 0.14+                    , vty >= 5.35 && < 5.38+    if !flag(examples)+      buildable: False++test-suite brick-panes-test+    import:           settings+    default-language: Haskell2010+    -- other-modules:+    -- other-extensions:+    type:             exitcode-stdio-1.0+    hs-source-dirs:   test+    main-is:          Main.hs+    build-depends:    base >= 4.13 && < 4.18+                    , brick-panes++
+ samples/mywork/Defs.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE UndecidableInstances #-}++module Defs where++import Brick hiding (Location)+import Brick.Focus+import Brick.Panes+import Brick.Widgets.Border+import Data.Text ( Text )+import Data.Time.Calendar+import GHC.Generics ( Generic )+import Lens.Micro+import Lens.Micro.Extras ( view )+++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+++numProjects :: Projects -> Int+numProjects = length . projects+++----------------------------------------------------------------------++data MyWorkCore = MyWorkCore { projFile :: FilePath+                             , myWorkFocus :: FocusRing WName+                             }++initMyWorkCore :: MyWorkCore+initMyWorkCore = MyWorkCore { projFile = "projects.json"+                            , myWorkFocus = focusRing [ WProjList, WLocation ]+                            }++coreWorkFocusL :: Lens' MyWorkCore (FocusRing WName)+coreWorkFocusL f c = (\f' -> c { myWorkFocus = f' }) <$> f (myWorkFocus c)+++data WName = WSummary | WProjList | WLocation | WNotes | WOps+           | WFileMgr | WFBrowser | WFSaveBtn+           | WPList | WPFilter | WLList+  deriving (Eq, Ord, Show)++++-- | Adds a border with a title to the current widget.  First argument is True if+-- the current widget has focus.+titledB :: Bool -> Text -> Widget WName -> Widget WName+titledB fcsd text =+  let ttlAttr = if fcsd then withAttr (attrName "Selected") else id+  in borderWithLabel (ttlAttr $ txt text)+++type MyWorkEvent = ()  -- No app-specific event for this simple app+++class HasProjects s where+  getProjects :: s -> (Bool, Projects)+++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))+++class HasSelection s where+  selectedProject :: s -> Maybe Text++instance ( PanelOps Projects WName MyWorkEvent panes MyWorkCore+         , HasSelection (PaneState Projects MyWorkEvent)+         )+  => HasSelection (Panel WName MyWorkEvent MyWorkCore panes) where+  selectedProject = selectedProject . view (onPane @Projects)
+ samples/mywork/InitialData.hs view
@@ -0,0 +1,90 @@+{-# LANGUAGE OverloadedStrings #-}++module InitialData+  (+    initial_projects+  )+where++import           Data.Time.Calendar++import           Defs+++initial_projects :: Projects+initial_projects = Projects+  [+    Project+    { name = "brick-panes"+    , description = "Compositional aspect isolation library for Brick-based TUI apps."+    , role = Author+    , language = Right Haskell+    , locations =+      [+        Location+        { location = "/home/kquick/Projects/panes"+        , locatedOn = Nothing+        , notes =+          [ Note+            { note = "Main development location"+            , notedOn = fromGregorian 2022 09 08+            }+          ]+        }+      , Location+        { location = "https://github.com/kquick/brick-panes"+        , locatedOn = Just $ fromGregorian 2022 09 10+        , notes =+          [ Note+            { note = "Published source code VCS location"+            , notedOn = fromGregorian 2022 09 10+            }+          ]+        }+      ]+    }+  +  , Project+    { name = "brick"+    , description = "A declarative terminal user interface library"+    , role = Contributor+    , language = Right Haskell+    , locations =+      [+        Location { location = "/home/kquick/Public/brick"+                 , locatedOn = Just $ fromGregorian 2022 08 28+                 , notes =+                           [ Note+                             { note = "local copy of master"+                             , notedOn = fromGregorian 2022 07 19+                             }+                           , Note+                             { note = "updated to post-1.0 Brick release"+                             , notedOn = fromGregorian 2022 08 28+                             }+                           ]+                 }+      , Location { location = "https://github.com/jtdaugherty/brick"+                 , locatedOn = Just $ fromGregorian 2017 12 27+                 , notes = mempty+                 }+      , Location { location = "https://github.com/kquick/brick"+                 , locatedOn = Nothing+                 , notes =+                   [ Note+                     { note = "Fork for pushing changes to create pull requests \n\+                              \ \n\+                              \ Remember to put changes on a branch!"+                     , notedOn = fromGregorian 2020 10 5+                     }+                   , Note+                     { note = "This repo's master is not always up to date.\n\+                              \ Usually only development branches are pushed, \+                              \ not master."+                     , notedOn = fromGregorian 2020 10 20+                     }+                   ]+                 }+      ]+    }+  ]
+ samples/mywork/Main.hs view
@@ -0,0 +1,145 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}++module Main where++import           Brick hiding ( Location )+import           Brick.Focus+import           Brick.Panes+import           Brick.Widgets.Border+import           Brick.Widgets.Border.Style+import           Brick.Widgets.Edit+import           Brick.Widgets.List+import           Control.Monad ( guard, when )+import           Control.Monad.IO.Class ( liftIO )+import qualified Data.List as DL+import           Data.Maybe ( catMaybes )+import           Data.Version ( showVersion )+import           Graphics.Vty ( defAttr, withStyle, defaultStyleMask+                              , bold, reverseVideo, dim+                              , black, white, yellow, red )+import qualified Graphics.Vty as Vty+import           Lens.Micro++import           Defs+import           Panes.Location ()+import           Panes.Operations+import           Panes.Projects ()+import           Panes.Summary+import           Panes.FileMgr+import           Paths_brick_panes ( version )++-- import           InitialData+-- import Data.ByteString.Lazy as BS+-- import Data.Aeson ( encode )++type MyWorkState = Panel WName MyWorkEvent MyWorkCore+                   '[ SummaryPane+                    , OperationsPane+                    , Location+                    , Projects+                    , FileMgrPane+                    ]++main :: IO ()+main = defaultMain myworkApp initialState >> return ()+-- main = BS.writeFile "projects.json" (encode $ initial_projects)++myworkApp :: App MyWorkState MyWorkEvent WName+myworkApp = App { appDraw = drawMyWork+                , appChooseCursor = showFirstCursor+                , appHandleEvent = handleMyWorkEvent+                , appStartEvent = return ()+                , appAttrMap = const myattrs+                }++myattrs :: AttrMap+myattrs = attrMap defAttr+          [+            (editAttr, white `on` black)+          , (editFocusedAttr, yellow `on` black)++          , (listAttr, defAttr `withStyle` defaultStyleMask)+          , (listSelectedAttr, defAttr `withStyle` bold)+          , (listSelectedFocusedAttr, defAttr `withStyle` reverseVideo)++          , (attrName "disabled", defAttr `withStyle` dim)+          , (attrName "Selected", black `on` yellow)+          , (attrName "Error", fg red)+          ]++initialState :: MyWorkState+initialState = focusRingUpdate myWorkFocusL+               $ addToPanel Never+               $ addToPanel Never+               $ addToPanel WhenFocused+               $ addToPanel WhenFocused+               $ addToPanel WhenFocusedModal+               $ basePanel initMyWorkCore++drawMyWork :: MyWorkState -> [Widget WName]+drawMyWork mws =+  let mainPanes =+        [+          borderWithLabel  (str $ " mywork " <> showVersion version <> " ")+          $ vBox $ catMaybes+          [+            panelDraw @SummaryPane mws+          , Just hBorder+          , Just $ hBox $ catMaybes+            [ hLimitPercent 25+              <$> panelDraw @Projects mws+            , Just vBorder+            , panelDraw @Location mws+            ]+          , Just hBorder+          , panelDraw @OperationsPane mws+          ]+        ]+      allPanes = catMaybes [ panelDraw @FileMgrPane mws+                           ]+                 <> mainPanes+      disableLower = \case+        (m:ls) -> m : (withDefAttr (attrName "disabled") <$> ls)+        o -> o+  in joinBorders . withBorderStyle unicode <$> disableLower allPanes+++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+    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 proj0 <- gets selectedProject+             s <- get+             (_,s') <- handleFocusAndPanelEvents myWorkFocusL s ev+             put s'+             (new,prjs) <- gets getProjects+             let mprj st = do pnm <- selectedProject st+                              guard (Just pnm /= proj0)+                              DL.find ((== pnm) . name) (projects prjs)+             when new $+               modify $ \st -> st+                               & focusRingUpdate myWorkFocusL+                               & onPane @Projects %~ updatePane prjs+                               & onPane @FileMgrPane %~ updatePane False+             modify $ \st ->+                        case mprj st of+                          Just p -> st & onPane @Location %~ updatePane p+                          _ -> st+             modify $ focusRingUpdate myWorkFocusL++myWorkFocusL :: Lens' MyWorkState (FocusRing WName)+myWorkFocusL = onBaseState . coreWorkFocusL
+ samples/mywork/Panes/FileMgr.hs view
@@ -0,0 +1,180 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE UndecidableInstances #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Panes.FileMgr+  (+    FileMgrPane+  , initFileMgr+  , myProjectsL+  )+where++import           Brick hiding ( Location )+import           Brick.Panes+import           Brick.Widgets.Center+import qualified Brick.Widgets.Core as BC+import           Brick.Widgets.FileBrowser+import qualified Control.Exception as X+import           Control.Monad.IO.Class ( liftIO )+import           Data.Aeson ( ToJSON, FromJSON, decode, encode )+import qualified Data.ByteString.Lazy as BS+import qualified Data.Sequence as Seq+import qualified Graphics.Vty as Vty+import           Lens.Micro+import           Lens.Micro.Extras ( view )+import           System.Directory ( doesDirectoryExist )++import           Defs+++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 _ = 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)+++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)+++drawFB :: DrawConstraints FileMgrPane drawstate WName+       => drawstate -> FileBrowser WName -> Widget WName+drawFB ds b =+  let width = 70+      fcsd = ds^.getFocus.to focused+      browserPane fb =+        let hasFocus = fcsd == Just WFBrowser+        in vLimitPercent 55 $ hLimitPercent width+           $ titledB hasFocus "Choose a file"+           $ renderFileBrowser hasFocus fb+      helpPane =+        padTop (BC.Pad 1) $ hLimitPercent width $ vBox+        [ hCenter $ txt "Up/Down: select"+        , hCenter $ txt "/: search, Ctrl-C or Esc: cancel search"+        , hCenter $ txt "Enter: change directory or select file"+        , hCenter $ txt "Space: change directory"+        , hCenter $ txt "TAB: select Save option"+        , hCenter $ txt "ESC: quit"+        ]+      errDisplay fb = case fileBrowserException fb of+                        Nothing -> emptyWidget+                        Just e -> hLimitPercent width+                                  $ withDefAttr (attrName "Error")+                                  $ strWrap+                                  $ X.displayException e+      savePane = (if fcsd == Just WFSaveBtn+                  then withAttr (attrName "Selected")+                  else id)+                 $ str "[SAVE]"+  in centerLayer (browserPane b <=> errDisplay b <=> savePane <=> helpPane)+++handleFileLoadEvent :: Vty.Event+                    -> PaneState FileMgrPane MyWorkEvent+                    -> EventM WName es (PaneState FileMgrPane MyWorkEvent)+handleFileLoadEvent ev ts =+  case ts^.fBrowser of+    Just fb -> do+      b <- nestEventM' fb $ handleFileBrowserEvent ev+      let selectFile =+            case fileBrowserCursor b of+              Nothing -> return $ ts & fBrowser .~ Just b  -- navigation+              Just f ->+                let fp = fileInfoFilePath f+                in liftIO $ doesDirectoryExist fp >>= \e ->+                  if e+                  then return $ ts & fBrowser .~ Just b+                  else do newprjs <- decode <$> liftIO (BS.readFile fp)+                          -- Setting fBrowser to Nothing: exit from modal+                          case newprjs of+                            Nothing -> return $ ts & fBrowser .~ Nothing+                            Just prjs -> return $ (ts { newProjects = True})+                                         & fBrowser .~ Nothing+                                         & myProjectsL .~ prjs+      case ev of+        Vty.EvKey Vty.KEnter [] -> selectFile+        Vty.EvKey (Vty.KChar ' ') [] -> selectFile+        _ -> return $ ts & fBrowser .~ Just b+    Nothing -> return ts  -- shouldn't happen+++handleFileSaveEvent :: Vty.Event+                    -> PaneState FileMgrPane MyWorkEvent+                    -> EventM WName es (PaneState FileMgrPane MyWorkEvent)+handleFileSaveEvent _ ts =+  case fileBrowserCursor =<< ts^.fBrowser of+    Nothing -> return ts+    Just f -> let fp = fileInfoFilePath f+              in liftIO (doesDirectoryExist fp) >>= \case+                    True -> return ts -- TODO: show error+                    False -> do liftIO $ BS.writeFile fp (encode $ myProjects ts)+                                return ts+++instance ToJSON Projects+instance ToJSON Project+instance ToJSON Role+instance ToJSON Language+instance ToJSON Location+instance ToJSON Note++instance FromJSON Projects+instance FromJSON Project+instance FromJSON Role+instance FromJSON Language+instance FromJSON Location+instance FromJSON Note+-- deriving via Generically Note instance ToJSON Note+++initFileMgr :: IO (PaneState FileMgrPane MyWorkEvent)+initFileMgr = do+  fb <- newFileBrowser selectNonDirectories WFBrowser Nothing+  return $ initPaneState fb & fBrowser .~ Just fb
+ samples/mywork/Panes/Location.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Panes.Location () where++import           Brick hiding ( Location )+import           Brick.Panes+import           Brick.Widgets.List+import           Lens.Micro+import qualified Data.List as DL+import           Data.Maybe ( fromMaybe )+import           Data.Text ( Text )+import           Data.Time.Calendar+import qualified Data.Vector as V++import           Defs+++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 $ snd $ 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+                      <+> vLimit 1 (fill ' ')+                      <+> (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)
+ samples/mywork/Panes/Operations.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeSynonymInstances #-}++module Panes.Operations+  (+    OperationsPane+  )+where++import Brick+import           Brick.Panes+import qualified Data.List as List++import           Defs+++data OperationsPane++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 " "
+ samples/mywork/Panes/Projects.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Panes.Projects () where++import           Brick+import           Brick.Panes+import           Brick.Widgets.Edit+import           Brick.Widgets.List+import qualified Data.Sequence as Seq+import           Data.Text ( Text )+import qualified Data.Text.Zipper as TZ+import qualified Data.Vector as V+import           Lens.Micro++import           Defs+++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 $ snd $ getProjects s+                        pl = list WPList (V.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+  type (UpdateType Projects) = Projects+  updatePane newprjs =+    (pList %~ listReplace (V.fromList (name <$> projects newprjs)) (Just 0))+    .+    (pSrch . editContentsL %~ TZ.clearZipper)+++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)+++instance HasSelection (PaneState Projects MyWorkEvent) where+  selectedProject = fmap snd . listSelectedElement . pL
+ samples/mywork/Panes/Summary.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeSynonymInstances #-}++module Panes.Summary+  (+    SummaryPane+  )+where++import           Brick+import           Brick.Panes+import qualified Data.List as List+import           Data.Maybe ( catMaybes )++import           Defs+++data SummaryPane++instance Pane WName MyWorkEvent SummaryPane where+  data (PaneState SummaryPane MyWorkEvent) = Unused+  type (DrawConstraints SummaryPane s WName) = ( HasProjects s )+  initPaneState _ = Unused+  drawPane _ s = Just $ drawSummary (snd $ getProjects s)+++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 5+       $ vBox+       [+         if null prjs+         then str "No projects defined"+         else prjcnt <+> fill ' ' <+> dateRange+       , str " "+       , str "Note: this is an example only and not fully functional."+       , strWrap "See https://github.com/kquick/mywork for a fully functional and feature-rich version."+       ]
+ src/Brick/Panes.hs view
@@ -0,0 +1,728 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++{-| This package provides an overlay library for Brick that allows+  individual TUI screen areas to be independently developed and then easily+  composed into the overall application.+-}++module Brick.Panes+  (+    -- * Pane Specification+    -- ** Definition and Initialization+    Pane+  , PaneState+  , InitConstraints+  , initPaneState+    -- ** Drawing+  , DrawConstraints+  , drawPane+    -- ** Event Handling+  , EventConstraints+  , EventType+  , DispatchEvent+  , focusable+  , handlePaneEvent+    -- ** Updating the Pane's state+  , UpdateType+  , updatePane+    -- ** Focus management helpers and constraints+  , focus1If+  , HasFocus+  , getFocus+  , Focused(Focused)+  , focused+    -- * Panel Specification+    -- ** Definition and Initialization+  , Panel+  , basePanel+  , addToPanel+  , PaneFocus( Always, Never, WhenFocused, WhenFocusedModal+             , WhenFocusedModalHandlingAllEvents+             )+    -- ** Pane and base state access+  , onPane+  , onBaseState+    -- ** Drawing+  , panelDraw+    -- ** Focus and Event management+  , handleFocusAndPanelEvents+  , focusRingUpdate+  , isPanelModal+  , enteredModal+  , exitedModal+  , PanelMode(Normal, Modal)+  , PanelTransition+    -- ** Access and operations+  , PanelOps(..)+  , PaneNumber+  )+where++import           Control.Applicative ( (<|>) )+import qualified Data.Foldable as F+import           Data.Kind ( Constraint, Type )+import qualified Data.List as L+import           Data.Maybe ( fromMaybe )+import           Data.Sequence ( Seq, (><) )+import qualified Data.Sequence as Seq+import           Data.Type.Equality+import           Data.Void ( Void, absurd )+import           GHC.TypeLits+import qualified Graphics.Vty as Vty+import           Lens.Micro+#if !MIN_VERSION_base(4,16,0)+-- starting in base 4.16.0.0, GHC.TypeLits exports Natural+import           Numeric.Natural ( Natural )+#endif++import           Brick+import           Brick.Focus+++-- | Class to manage each pane in the Brick TUI.+--+-- Type parameters:+--+--  *  @pane@ = Pane Type, uniquely identifying this pane+--  *  @appEv@ = The application's event type+--  *  @n@ = Widget type parameter+--+-- The 'PaneState' specifies the state that should be stored globally+-- and which provides the primary information for handling this pane+-- (for both draw and event handling operations).+--+-- The 'initPaneState' method is responsible for returning an initial 'PaneState'+-- value (at startup).+--+-- The 'drawPane' method is called to render the pane into a 'Widget' (or Nothing+-- if this Pane should not currently be drawn).  It is passed the 'PaneState' and+-- also a drawing parameter.  The 'DrawConstraints' can be used to specify+-- additional instance requirements for the drawing parameter.  The global+-- application state is often passed as this drawing parameter, but the+-- 'drawPane' method should only perform 'DrawConstraints' operations, along with+-- general Brick drawing operations.+--+-- The 'focusable' method should return the names of the widgets that can be the+-- target of the 'FocusRing' in the current state.  This should always return an+-- empty list if the 'drawPane' returns 'Nothing'.+--+-- The 'handlePaneEvent' method is called to handle an event that has occurred+-- within this Pane.  It should return the updated 'PaneState' in the context of+-- an 'EventM' monadic operation.+--+-- The 'updatePane' method is called with the 'UpdateType' to perform any+-- updating of the 'PaneState' from the update type data.+class Pane n appEv pane | pane -> n where++  -- | State information associated with this pane+  data (PaneState pane appEv)++  -- | Type of data provided to updatePane+  type (UpdateType pane)++  -- | Constraints on argument passed to 'initPaneState'.  If there are no+  -- constraints, this may be specified as @()@, or simply omitted because @()@+  -- is the default.+  type (InitConstraints pane initctxt) :: Constraint+  -- | Function called to initialize the internal 'PaneState'+  initPaneState :: (InitConstraints pane i) => i -> PaneState pane appEv++  -- | Constraints on the @drawcontext@ parameter passed to 'drawPane'.+  type (DrawConstraints pane drwctxt n) :: Constraint+  -- | Function called to draw the 'Pane' as a Brick 'Widget', or 'Nothing' if+  -- this 'Pane' should not be drawn at the current time.+  drawPane :: (DrawConstraints pane drawcontext n, Eq n)+           => PaneState pane appEv -> drawcontext -> Maybe (Widget n)++  -- | The constraints that should exist on the 'eventcontext' argment passed to+  -- 'focusable' and 'handlePaneEvent'.+  type (EventConstraints pane evctxt) :: Constraint+  -- | The type of the event argument delivered to 'handlePaneEvent'.  This+  -- should either be 'Vty.Event' or 'BrickEvent', depending on what level of+  -- granularity the 'handlePaneEvent' operates at.+  type (EventType pane n appEv)+  -- | The 'focusable' method is called to determine which Widget targets should+  -- be part of the Brick 'FocusRing'.+  focusable :: (EventConstraints pane eventcontext, Eq n)+            => eventcontext -> PaneState pane appEv -> Seq.Seq n+  -- | Called to handle an 'EventType' event for the 'Pane'.  This is typically+  -- only called when (one of the 'focusable' targets of) the 'Pane' is the focus+  -- of the 'FocusRing'.  It should modify the internal 'PaneState' as+  -- appropriate and make any appropriate changes to properly render the 'Pane'+  -- on the next 'drawPane' call.+  --+  -- Note that this function also receives an eventcontext which it may stipulate+  -- constraints on.  Those constraints should be *read-only* constraints.  This+  -- is especially important when the pane is used as part of a panel: the Panel+  -- itself is passed as the eventcontext, but the panel may not be modified+  -- because the panel event dispatching will discard any changes on completion.+  handlePaneEvent :: (EventConstraints pane eventcontext, Eq n)+                  => eventcontext+                  -> EventType pane n appEv+                  -> PaneState pane appEv+                  -> EventM n es (PaneState pane appEv)+  -- | Function called to update the internal 'PaneState', using the passed+  -- 'updateType' argument.+  updatePane :: UpdateType pane+             -> PaneState pane appEv+             -> PaneState pane appEv++  -- A set of defaults that allows a minimal instance specification+  type (UpdateType pane) = ()+  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+  updatePane _ = id+++-- | This is a helper function for a Pane with a single Widget name and a+-- conditional focus.  For example, if a widget is always focusable, then it can+-- specify:+--+--  > instance Pane N E ThisPane () where+--  >   ...+--  >   focusable _ = const $ focus1If MyWidgetName True+focus1If :: n -> Bool -> Seq.Seq n+focus1If n b = if b then Seq.singleton n else mempty+++-- | This class allows retrieval of the current focused Widget (if any).  This+-- class is frequently specified as one of the constraints for the+-- 'DrawConstraints' or 'EventConstraints' of a 'Pane'.+class HasFocus b n | b -> n where+  -- | Provides a lens from the primary type to the 'Focused' type, which+  -- specifies the current focused element (if any).+  getFocus :: Lens' b (Focused n)+  -- By default, nothing has Focus+  getFocus f x = const x <$> f (Focused Nothing)++-- | This is a newtype to wrap the identification of the current focused element+-- (if any).+newtype Focused n = Focused { focused :: Maybe n+                              -- ^ The current focused element or 'Nothing'.+                            }+++-- | The 'DispatchEvent' class is used to determine which type of event to+-- dispatch to a 'Pane' by selecting on the @'EventType' pane n@.  This is used+-- internally in the brick-panes implementation and client code does not need to+-- explicitly specify instances of this class.+class DispatchEvent n appev pane evtype where+  dispEv :: ( Pane n appev pane+            , EventConstraints pane base+            , Eq n+            )+         => EventType pane n appev :~: evtype+         -> base -> BrickEvent n appev -> PaneState pane appev+         -> EventM n es (PaneState pane appev)++instance DispatchEvent n appev pane (BrickEvent n appev) where+  dispEv Refl base ev s = handlePaneEvent base ev s++instance DispatchEvent n appev pane Vty.Event where+  dispEv Refl base ev s = case ev of+    VtyEvent vev -> handlePaneEvent base vev s+    _ -> return s+++----------------------------------------------------------------------+-- A Panel is a composite of a number of panes++-- | A Panel is a recursive data sequence of individual 'Pane' elements+-- with a core state.  The core state represents the base state of the+-- Brick application, independent of the various Pane data.  Each 'Pane'+-- has an instance that defines its 'PaneState', which is associated+-- here with a potential Widget name (allowing selected actions; see+-- 'handleFocusAndPanelEvents').+--+-- The 'Panel' type closes over the 'state' type argument, which is used for all+-- three of the 'Pane' constraints ('DrawConstraints', 'EventConstraints', and+-- indirectly the 'InitConstraints'), which means that the same 'state' type must+-- be passed to all three associated Pane methods; a 'Pane' used outside of the+-- 'Panel' container is not constrained in this manner and each method could have+-- a different argument.  For the Panel, the 'state' is typically the Panel+-- "beneath" the current Pane, which is the aggregate of the base state and all+-- Panes added before the current pane.+data Panel n appev state (panes :: [Type]) where+  Panel :: state -> Panel n appev state '[]+  PanelWith :: ( Pane n appev pane+               , DrawConstraints pane (Panel n appev state panes) n+               , EventConstraints pane (Panel n appev state panes)+               , DispatchEvent n appev pane (EventType pane n appev)+               )+            => PaneState pane appev -> PaneFocus n+            -> Panel n appev state panes -> Panel n appev state (pane ': panes)+++-- | This is the base constructor for Panel that is given the core+-- application state.+basePanel :: state -> Panel n appev state '[]+basePanel = Panel+++-- | Each 'Pane' that is part of the 'Panel' should be added to the 'Panel' via+-- this function, which also specifies when the `Pane` should receive Events.+addToPanel :: Pane n appev pane+           => InitConstraints pane (Panel n appev state panes)+           => DrawConstraints pane (Panel n appev state panes) n+           => EventConstraints pane (Panel n appev state panes)+           => DispatchEvent n appev pane (EventType pane n appev)+           => PaneFocus n+           -> Panel n appev state panes+           -> Panel n appev state (pane ': panes)+addToPanel n pnl = PanelWith (initPaneState pnl) n pnl+++-- | Specifies when a Pane should receive events.+data PaneFocus n =+  -- | Indicates that this Pane always receives all events, although it is never+  --   part of a focus ring.  This should be used for Widgets that have a global+  --   event handling.+  Always+  -- | Indicates that this Pane's handlePaneEvent is never called+  | Never+  -- | Indicates that the pane should receive events when the current focus is+  --   equal to a 'focusable' return from the Pane.+  | WhenFocused+  -- | Indicates that the pane should receive events when the current focus is+  --   equal to a 'focusable' return from the Pane, and that this should block+  --   all non-modal focus candidates (it is expected that there is only one+  --   modal, but this is not required).+  | WhenFocusedModal+  | WhenFocusedModal' (FocusRing n)  -- previous focus ring to return to+  -- | Indicates that the pane should receive events when the current focus is+  -- equal to a 'focusable' return from the Pane, and that this should block all+  -- non-modal focus candidates, just as with 'WhenFocusedModal'.  However, this+  -- also sends *all* events to the modal Pane instead of the normal 'Panel'+  -- handling of events (e.g.  @TAB@/@Shift-TAB@).+  | WhenFocusedModalHandlingAllEvents+  | WhenFocusedModalHandlingAllEvents' (FocusRing n)  -- previous focus ring+++-- | If the base state provides Focus information, then the Panel can provide+-- focus information.+instance HasFocus appState n => HasFocus (Panel n appEv appState panes) n where+  getFocus = onBaseState . getFocus+++-- | This is a lens providing access to the base application state at+-- the core of the Panel.+onBaseState :: Lens' (Panel n appev state panes) state+onBaseState f (Panel s) = Panel <$> f s+onBaseState f (PanelWith p n i) = PanelWith p n <$> onBaseState f i++-- | This is a lens providing access to the PaneState for a specific Pane in the+-- Panel.  The Pane is typically specified via a type application+-- (e.g. @@MyPane@).+onPane :: forall pane n appev state panes .+          PanelOps pane n appev panes state+       => Lens' (Panel n appev state panes) (PaneState pane appev)+onPane = lens (panelState @pane) (panelStateUpdate @pane)+++-- -- | This can be used to get the inner Pane from the current Pane in the state.+-- onNextPane :: Lens' (Panel n appev state (pane ': panes)) (Panel n appev state panes)+-- onNextPane f = \case+--   PanelWith a b r -> (\r' -> PanelWith a b r') <$> f r+++-- | This class defines the various operations that can be performed+-- on a Panel.  Most of these operations specify a particular Pane as+-- the target of the operation; the operation is performed on that+-- pane and the Panel is is updated with the result.+--+-- The user of this library will not need to develop new instances of this class:+-- the instances defined internally are sufficient.  Users may need to specify+-- 'PanelOps' constraints on various functions.+class PanelOps pane n appev panes s | pane -> n where++  -- | This is called to pass the VTY Event to the specified Pane's+  -- handler with a Panel.+  handlePanelEvent :: (EventConstraints pane s, Eq n)+                   => s -> pane -> Panel n appev s panes -> BrickEvent n appev+                   -> EventM n es (Panel n appev s panes)++  -- | This is used to obtain the state of a specific Pane within the Panel.  The+  -- pane is usually specified by a type application (e.g. @@MyPane@).+  panelState :: Panel n appev s panes -> PaneState pane appev++  -- | This is used to update the state of a specific Pane within the Panel. The+  -- pane is usually specified by a type application (e.g. @@MyPane@).+  panelStateUpdate :: Panel n appev s panes -> PaneState pane appev+                   -> Panel n appev s panes++  -- | This returns an ordinal index of the pane within the panel.+  paneNumber :: Panel n appev s panes -> PaneNumber+++instance (Pane n appev pane) => PanelOps pane n appev (pane ': panes) s where+  handlePanelEvent s _p (PanelWith pd n r) ev =+    (\pd' -> PanelWith pd' n r) <$> dispEv Refl s ev pd+  panelState (PanelWith pd _ _) = pd+  panelStateUpdate (PanelWith _pd n r) = \pd' -> PanelWith pd' n r+  paneNumber _ = PaneNo 0+++instance {-# OVERLAPPABLE #-} (PanelOps pane n appev panes s) =>+  PanelOps pane n appev (o ': panes) s where+  handlePanelEvent s p (PanelWith pd n r) ev =+    PanelWith pd n <$> handlePanelEvent s p r ev+  panelState (PanelWith _ _ r) = panelState r+  panelStateUpdate (PanelWith pd n r) =+    \pd' -> PanelWith pd n $ panelStateUpdate r pd'+  paneNumber (PanelWith _ _ r) = succ $ paneNumber @pane r+++instance ( TypeError+           ('Text "No " ':<>: 'ShowType pane ':<>: 'Text " in Panel"+            ':$$: 'Text "Add this pane to your Panel (or move it lower)"+            ':$$: 'Text "(Possibly driven by DrawConstraints)"+           )+         , Pane n appev pane+         )+  => PanelOps pane n appev '[] s where+  handlePanelEvent = absurd (undefined :: Void)+  panelState = absurd (undefined :: Void)+  panelStateUpdate = absurd (undefined :: Void)+  paneNumber = absurd (undefined :: Void)+++-- | Called to draw a specific pane in the panel.  Typically invoked from the+-- applications' global drawing function.+panelDraw :: forall pane n appev s panes .+             ( DrawConstraints pane (Panel n appev s panes) n+             , PanelOps pane n appev panes s+             , Pane n appev pane+             , Eq n+             )+          => Panel n appev s panes -> Maybe (Widget n)+panelDraw panel = drawPane (panelState @pane panel) panel+++-- | Called to dispatch an events to the focused Pane in the Panel as determined+-- by matching the Widget names returned by the Pane's 'focusable' with the+-- current FocusRing focus target.+handlePanelEvents :: Eq n+                  => Panel n appev s panes+                  -> BrickEvent n appev+                  -> Focused n+                  -> EventM n es (Panel n appev s panes)+handlePanelEvents panel ev (Focused focus) =+  -- n.b. no need to check focusable for a pane because an invisible+  -- pane should never have focus+  case focus of+    Nothing -> return panel+    Just fcs -> go fcs panel ev+  where+    go :: Eq n+       => n -> Panel n appev s panes -> BrickEvent n appev+       -> EventM n es (Panel n appev s panes)+    go _ p@(Panel {}) _ = return p+    go fcs (PanelWith pd pf r) evnt =+      let handleIt = dispEv Refl r evnt pd+          skipIt = return pd+      in do pd' <- case pf of+                     Never -> skipIt+                     Always -> handleIt+                     WhenFocused -> if fcs `elem` focusable r pd+                                    then handleIt+                                    else skipIt+                     WhenFocusedModal -> if fcs `elem` focusable r pd+                                         then handleIt+                                         else skipIt+                     WhenFocusedModal' _ -> if fcs `elem` focusable r pd+                                            then handleIt+                                            else skipIt+                     WhenFocusedModalHandlingAllEvents ->+                       if fcs `elem` focusable r pd+                       then handleIt+                       else skipIt+                     WhenFocusedModalHandlingAllEvents' _ ->+                       if fcs `elem` focusable r pd+                       then handleIt+                       else skipIt+            PanelWith pd' pf <$> go fcs r evnt+++-- | Called to handle events for the entire 'Panel', including focus-changing+-- events.  The current focused 'Pane' is determined and that Pane's handler is+-- called (based on the 'Widget' names returned as 'focusable' for that Pane).+-- If a Pane has no associated Widget name (the 'PaneFocus' value is specified as+-- 'Nothing' when adding the Pane to the Panel) then its handler is never called.+--+-- This function returns the updated Panel state, as well as an indication of+-- whether a modal transition occured while handling the event.+--+-- This function manages updating the focus when @Tab@ or @Shift-Tab@ is+-- selected, except when the currently focused pane was created with the+-- 'WhenFocusedModalHandlingAllEvents', in which case all events are passed+-- through to the Pane.+handleFocusAndPanelEvents :: Eq n => Ord n+                          => Lens' (Panel n appev s panes) (FocusRing n)+                          -> Panel n appev s panes+                          -> BrickEvent n appev+                          -> EventM n es (PanelTransition, Panel n appev s panes)+handleFocusAndPanelEvents focusL panel =+  let fcs = focusGetCurrent (panel ^. focusL)+      doPanelEvHandling = case fcs of+                            Nothing -> True+                            Just curFcs -> chkEv curFcs panel+  in \case+    VtyEvent (Vty.EvKey (Vty.KChar '\t') []) | doPanelEvHandling ->+      return (Nothing, panel & focusL %~ focusNext)+    VtyEvent (Vty.EvKey Vty.KBackTab []) | doPanelEvHandling ->+      return (Nothing, panel & focusL %~ focusPrev)+    panelEv -> do+      u <- focusRingUpdate focusL <$> handlePanelEvents panel panelEv (Focused fcs)+      let fcs' = focusGetCurrent (u ^. focusL)+      if fcs == fcs'+        then return (Nothing, u)+        else let m0 = panelMode focusL panel+                 m1 = panelMode focusL u+             in return $ if m0 == m1+                         then (Nothing, u)+                         else (Just (m0, m1), u)+  where+    chkEv :: Eq n => n -> Panel n appev s panes -> Bool+    chkEv curFcs = \case+      Panel {} -> True+      PanelWith pd WhenFocusedModalHandlingAllEvents r ->+        (not $ curFcs `elem` focusable r pd) && chkEv curFcs r+      PanelWith pd (WhenFocusedModalHandlingAllEvents' _) r ->+        (not $ curFcs `elem` focusable r pd) && chkEv curFcs r+      PanelWith _ _ r -> chkEv curFcs r+++panelMode :: Eq n+          => Ord n+          => Lens' (Panel n appev s panes) (FocusRing n)+          -> Panel n appev s panes -> PanelMode+panelMode focusL panel =+  modalTgt (L.sort $ focusRingToList (panel ^. focusL)) panel+  -- Note that the focusL-retrieved focus rings (m0, at least)+  -- come from the live previous pane set and may not match the+  -- order of the widget set.  Oddly, it doesn't match a rotation+  -- of the original either, ergo the sorting.+  where+    modalTgt :: Eq n+             => Ord n+             => [n] -> Panel n appev s panes -> PanelMode+    modalTgt fcsRing = \case+      Panel {} -> Normal+      PanelWith pd WhenFocusedModal r ->+        matchOrRecurse fcsRing r $ focusable r pd+      PanelWith pd (WhenFocusedModal' _) r ->+        matchOrRecurse fcsRing r $ focusable r pd+      PanelWith pd WhenFocusedModalHandlingAllEvents r ->+        matchOrRecurse fcsRing r $ focusable r pd+      PanelWith pd (WhenFocusedModalHandlingAllEvents' _) r ->+        matchOrRecurse fcsRing r $ focusable r pd+      PanelWith _ _ r -> case modalTgt fcsRing r of+                           Normal -> Normal+                           Modal p -> Modal $ succ p+    matchOrRecurse :: Eq n => Ord n+                   => [n] -> Panel n appev s pnlpanes -> Seq.Seq n -> PanelMode+    matchOrRecurse fcsRing r f =+      -- if fcsRing `elem` rotations (F.toList f)+      if fcsRing == L.sort (F.toList f)+      then Modal (PaneNo 0)+      else case modalTgt fcsRing r of+             Normal -> Normal+             Modal p -> Modal $ succ p+++-- | This function can be called at any time to determine if the Panel is+-- currently displaying a Modal Pane.  This needs the Panel object and a lens+-- that can be used to extract the FocusRing from the Panel.+isPanelModal :: Eq n+            => Ord n+            => Lens' (Panel n appev s panes) (FocusRing n)+            -> Panel n appev s panes+            -> Bool+isPanelModal focusL panel = Normal /= panelMode focusL panel+++-- | Indicates the current mode of the Panel.  If Modal, the currently active+-- modal Panel is identified by the PaneNumber, which matches the return value of+-- the 'paneNumber' of PanelOps; in general, the use of 'isPaneModal' is+-- recommended over attempting to determine _which_ actual modal pane is active.+data PanelMode = Normal | Modal PaneNumber deriving (Eq)++-- | Internal bookkeeping to identify a particular Pane within a Panel by number.+newtype PaneNumber = PaneNo Natural deriving (Eq, Enum)+++-- | This is returned from the 'handleFocusAndPanelEvents' function to indicate+-- whether a modal transition occured during the panel's (and associated Pane's)+-- handling of this event.  This can be used by the outer-level application code+-- to determine if a modal Pane was entered or exited due to the Event.+type PanelTransition = Maybe (PanelMode, PanelMode)+++-- | Indicates if the specified Pane (via Type Application) is the one that was+-- modally entered as a result of processing an event (as indicated by+-- PanelTransition).+enteredModal :: forall pane n appev state panes+               . PanelOps pane n appev panes state+               => PanelTransition -> Panel n appev state panes -> Bool+               -- n.b. assumes the Panel passed here is the same panel passed to+               -- handleFocusAndPanelEvents for which the PanelTransition was+               -- obtained+enteredModal = \case+  Just (_, Modal pnum) -> (pnum ==) . paneNumber @pane+  _ -> const False+++-- | Indicates if the specified Pane (via Type Application) is the one that was+-- modally exited (dismissed) as a result of processing an event (as indicated by+-- PanelTransition).+exitedModal :: forall pane n appev state panes+               . PanelOps pane n appev panes state+               => PanelTransition -> Panel n appev state panes -> Bool+               -- n.b. assumes the Panel passed here is the same panel passed to+               -- handleFocusAndPanelEvents for which the PanelTransition was+               -- obtained+exitedModal = \case+  Just (Modal pnum, _) -> (pnum ==) . paneNumber @pane+  _ -> const False+++-- | When the Panel is managing focus events (e.g. when using+-- 'handleFocusAndPanelEvents'), this function can be called if there+-- has been a situation where the members of the focus ring might need+-- to be updated.  This is automatically called at the end of the+-- 'handleFocusAndPanelEvents', but it should be explicitly called+-- once when the Panel is initialized, and it can additionally be+-- called whenever needed in a situation where the+-- 'handleFocusAndPanelEvents' invocation is insufficient (e.g. a+-- separate global action enables a modal pane).+focusRingUpdate :: (Eq n, Ord n)+                => Lens' (Panel n appev s panes) (FocusRing n)+                -> Panel n appev s panes -> Panel n appev s panes+focusRingUpdate focusL panel = let (p', r) = focusableNames focusL panel+                               in p' & focusL %~ updRing r+  where+    updRing :: Eq n => [n] -> FocusRing n -> FocusRing n+    updRing nl fcs =+      case nl of+        [] -> focusRing []+        (n : _) ->+          case focusGetCurrent fcs of+            Nothing ->+              -- no current focus, just use new list+              focusSetCurrent n $ focusRing nl+            Just e ->+              case L.find ((e ==) . head) $ rotations nl of+                Just r ->+                  focusRing r -- new ring with current element still focused+                Nothing ->+                  -- new focus ring doesn't include current focused+                  -- element, so just use the new list.+                  focusSetCurrent n $ focusRing nl+++-- | This returns the focusable Widget names for the focus ring, in the 'Ord'+-- order.  It also returns an updated panel, which internally records the input+-- focus ring if a modal is selected).  If the previous focus was a modal and the+-- new focus is not modal, this will return that previous focus ring rather than+-- the computed focus ring.+focusableNames :: (Eq n, Ord n)+               => Lens' (Panel n appev s panes) (FocusRing n)+               -> Panel n appev s panes -> (Panel n appev s panes, [n])+focusableNames focusL panel = finish $ subFocusable focusL panel panel+  where+    finish ((prvFcs, pnl), (mdlFcs, regFcs)) =+      let reorder = F.toList . Seq.sort+          fr = if null mdlFcs+               then fromMaybe (reorder regFcs) prvFcs+               else reorder mdlFcs+      in (pnl, fr)++subFocusable :: Eq n+             => Lens' (Panel n appev s panes) (FocusRing n)+             -> Panel n appev s panes -> Panel n appev s rempanes+             -> ((Maybe [n], Panel n appev s rempanes), (Seq n, Seq n))+subFocusable focusL base = \case+  i@(Panel {}) -> ((Nothing, i), (mempty, mempty))+  PanelWith pd WhenFocused r ->+    let (i', ns) = subFocusable focusL base r+        ns' = let pf = focusable r pd+              in (fst ns, pf >< snd ns)+    in (PanelWith pd WhenFocused <$> i', ns')+  PanelWith pd WhenFocusedModal r ->+    let (f', pf', i', ns') = goModal focusL base pd Nothing r+        pfNew = case pf' of+                  Nothing -> WhenFocusedModal+                  Just x -> WhenFocusedModal' x+    in ((f', PanelWith pd pfNew i'), ns')+  PanelWith pd (WhenFocusedModal' pf) r ->+    let (f', pf', i', ns') = goModal focusL base pd (Just pf) r+        pfNew = case pf' of+                  Nothing -> WhenFocusedModal+                  Just x -> WhenFocusedModal' x+    in ((f', PanelWith pd pfNew i'), ns')+  PanelWith pd WhenFocusedModalHandlingAllEvents r ->+    let (f', pf', i', ns') = goModal focusL base pd Nothing r+        pfNew = case pf' of+                  Nothing -> WhenFocusedModalHandlingAllEvents+                  Just x -> WhenFocusedModalHandlingAllEvents' x+    in ((f', PanelWith pd pfNew i'), ns')+  PanelWith pd (WhenFocusedModalHandlingAllEvents' pf) r ->+    let (f', pf', i', ns') = goModal focusL base pd (Just pf) r+        pfNew = case pf' of+                  Nothing -> WhenFocusedModalHandlingAllEvents+                  Just x -> WhenFocusedModalHandlingAllEvents' x+    in ((f', PanelWith pd pfNew i'), ns')+  PanelWith x y r -> let (i', ns) = subFocusable focusL base r+                     in (PanelWith x y <$> i', ns)++goModal :: fullpanel ~ Panel n appev s panes+        => rempanel ~ Panel n appev s rempanes+        => EventConstraints pane rempanel+        => Pane n appev pane+        => Eq n+        => Lens' fullpanel (FocusRing n)+        -> fullpanel+        -> PaneState pane appev+        -> Maybe (FocusRing n)+        -> rempanel+        -> (Maybe [n], Maybe (FocusRing n), rempanel, (Seq n, Seq n))+goModal focusL base pd pf r =+      let ((f, i'), ns) = subFocusable focusL base r+          fnms = focusable r pd+          fpred = not $ Seq.null fnms+          ns' =  (fnms >< fst ns, snd ns)+          f' = if fpred then Nothing else f <|> (focusRingToList <$> pf)+          pf' = if fpred then pf <|> Just (base^.focusL) else Nothing+      in (f', pf', i', ns')++++-- | This returns all shrl instances of the input list.+--+--  rotations [1,2,3] == [ [1,2,3], [2,3,1], [3,1,2] ]+--  rotations [1,2,3,4] == [ [1,2,3,4], [2,3,4,1], [3,4,1,2], [4,1,2,3] ]+--  rotations [1] == [ [1] ]+--  rotations [] == []+rotations :: [a] -> [ [a] ]+rotations l = map rotateBy $ [0..length l - 1]+  where rotateBy n = uncurry (flip (<>)) $ L.splitAt n l
+ test/Main.hs view
@@ -0,0 +1,4 @@+module Main (main) where++main :: IO ()+main = putStrLn "Test suite not yet implemented."