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 +28/−0
- LICENSE +13/−0
- README.org +1324/−0
- brick-panes.cabal +96/−0
- samples/mywork/Defs.hs +107/−0
- samples/mywork/InitialData.hs +90/−0
- samples/mywork/Main.hs +145/−0
- samples/mywork/Panes/FileMgr.hs +180/−0
- samples/mywork/Panes/Location.hs +50/−0
- samples/mywork/Panes/Operations.hs +34/−0
- samples/mywork/Panes/Projects.hs +63/−0
- samples/mywork/Panes/Summary.hs +56/−0
- src/Brick/Panes.hs +728/−0
- test/Main.hs +4/−0
+ 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."