packages feed

brick-3.0: src/Brick/Widgets/MenuBar.hs

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -fno-warn-unused-top-binds #-}
-- | This module provides a menu bar for grouping menus together.
--
-- Menu bars carry menus of a particular type using the menu types
-- provided in the @Brick.Widgets.Menu@ module. The type aliases
-- provided here correspond to the aliases for menu use cases:
--
-- * 'SimpleMenuBar': a menu bar made up of 'SimpleMenu's created with
--   'simpleMenu'
-- * 'DispatchingMenuBar': a menu bar made up of 'DispatchingMenu's
--    created with 'menuWithDispatcher'
-- * 'MenuBar': the fully general type for menu bars with menus created
--   with 'menu'
--
-- In all cases, use 'newMenuBar' to construct a menu bar, and create
-- its menus using the corresponding menu constructor for the type of
-- menu bar you want to use.
--
-- Render the menu bar with 'renderMenuBar' and handle menu bar events
-- with 'handleMenuBarEvent', deferring to the application's event
-- handling for events that the menu bar doesn't handle.
--
-- Similar to individual menus, menu bars have an orientation that can
-- be changed with 'setMenuBarOrientation'.
--
-- This API requires the use of lenses for application state fields that
-- store menu bar state.
--
-- See the @MenuBarDemo@ demonstration program for a complete working
-- example of using this API.
--
-- = Adding a Menu Bar to An Application
--
-- To use this module in an application:
--
-- * Choose a menu bar type that you want to work with such as
--   'SimpleMenuBar'.
-- * Add an application state field and lens for a value of the menu
--   bar's type, and add a constructor to the application's resource
--   name type, with an argument of type 'MenuRegion', for each menu
--   in the menu bar. Add lenses to the application state type with
--   'Lens.Micro.TH.makeLenses'.
-- * Populate the application's initial state with the menu bar.
-- * Render the menu bar with 'renderMenuBar'.
-- * Handle incoming events first with 'handleMenuBarEvent', and when
--   'handleMenuBarEvent' returns @False@, pass unhandled events on to
--   the existing application event handler.
module Brick.Widgets.MenuBar
  (
  -- * Types
    MenuBar
  , SimpleMenuBar
  , DispatchingMenuBar

  -- * Creating menu bars
  , newMenuBar

  -- * Handling events
  , handleMenuBarEvent

  -- * Rendering
  , renderMenuBar

  -- * Working with menu bars
  , hasOpenMenu
  , closeAllMenus
  , openMenuAtIndex
  , toggleMenuAtIndex
  , setMenuBarOrientation
  )
where

import Control.Monad (when)
import Data.Maybe (isJust, listToMaybe, fromMaybe)
import Lens.Micro.Platform ((^.), (&), (%~), (.~), Lens', ix, each)
import Lens.Micro.Mtl

import qualified Data.Foldable as F
import qualified Data.Vector as V

import qualified Graphics.Vty as Vty

import Brick.Types
import Brick.Widgets.Core
import Brick.Widgets.Menu

-- | A menu bar holding a sequence of menus.
--
-- A menu bar can have up to one open menu at a time.
data MenuBar s n k =
    MenuBar { menuBarOrientation :: !MenuOrientation
            , menuBarMenus :: !(V.Vector (Menu s n k))
            }

suffixLenses ''MenuBar

-- | A specialization of 'MenuBar' for menus with 'EventM' handlers; use
-- this with 'simpleMenu'.
type SimpleMenuBar s n = MenuBar s n (EventM n s ())

-- | A specialization of 'MenuBar' for menus with abstract key event
-- triggers; this with 'menuWithDispatcher'.
type DispatchingMenuBar s n k = MenuBar s n (EventM n s (EntryTrigger s n k))

-- | Create a new menu bar from the specified menu list. If the list is
-- empty, this calls 'error'.
newMenuBar :: [Menu s n k] -> MenuBar s n k
newMenuBar [] = error "BUG: newMenuBar requires a non-empty list"
newMenuBar ms = MenuBar LeftToRight $ V.fromList ms

-- | Return whether this menu bar has an open menu.
hasOpenMenu :: MenuBar s n k -> Bool
hasOpenMenu = isJust . getOpenMenu

-- | Get this menu bar's current open menu and its index, if any.
getOpenMenu :: MenuBar s n k -> Maybe (Int, Menu s n k)
getOpenMenu mb = do
    let ms = menuBarMenus mb
    idx <- V.findIndex menuIsOpen ms
    return (idx, ms V.! idx)

-- | Render this menu bar with the given application state as input.
renderMenuBar :: (Ord n) => s -> MenuBar s n k -> Widget n
renderMenuBar s mb =
    withDefAttr menuTitleAttr $ padForOrientation body
    where
        padForOrientation = case mb^.menuBarOrientationL of
            LeftToRight -> padRight Max
            RightToLeft -> padLeft Max . padRight (Pad 1)

        body = hBox $
               padLeft (Pad 1) <$>
               F.toList (renderMenu s <$> menuBarMenus mb)

-- | Given a resource name, find the menu whose title bar portion
-- matches the resource name, if any.
getMenuTitleMatch :: (Eq n) => MenuBar s n k -> n -> Maybe (Int, Menu s n k)
getMenuTitleMatch mb n =
    listToMaybe $ filter matchesTitle $ zip [0..] (F.toList $ mb^.menuBarMenusL)
    where
        matchesTitle (_, m) = n == menuTitleName m

-- | Handle an event for this menu bar and return @True@, or return
-- @False@ if the event was not handled (e.g. because the event was not
-- a menu title mouse click or because no menu was open to receive the
-- event).
--
-- Events handled include:
--
-- * Mouse clicks on menu titles will open the clicked menu, closing
--   other open menus.
-- * Left and Right arrow keys will cycle between menus if there is an
--   open menu.
-- * If a submenu entry is selected, the arrow keys will open it or
--   close it if it is open, depending on the configured menu bar
--   orientation.
-- * @Esc@ will close the currently-open menu.
--
-- In all other cases, this will attempt to defer to the opened menu to
-- handle the event. This returns @True@ if the event was one of the
-- above and was handled, @True@ if the event was not one of the above
-- but was handled by the open menu, or @False@ otherwise.
--
-- A return value of @True@ indicates that the event should not be
-- handled by the application because it was destined for the menu bar
-- or one of its menus; a return value of @False@ indicates that the
-- event should be handled by the application because it did not affect
-- the menu bar or its menus in their current state for any reason.
-- Consequently, a common pattern when using this function will look
-- something like this:
--
-- @
-- myApplicationEventHandler :: BrickEvent n e -> EventM n s ()
-- myApplicationEventHandler e = do
--     handled <- handleMenuBarEvent myMenuBarLens e
--     when (not handled) $ do
--         -- Go on to handle the event in the rest of the application
-- @
handleMenuBarEvent :: (Eq n)
                   => Lens' s (MenuBar s n k)
                   -- ^ The lens into the application state where the
                   -- menu state can be found
                   -> BrickEvent n e
                   -- ^ The event to handle
                   -> EventM n s Bool
handleMenuBarEvent which e@(VtyEvent (Vty.EvKey Vty.KLeft [])) = do
    -- Since this key might be handled by the open menu, try that first
    -- and only switch menus if it wasn't handled by the menu.
    handled <- withOpenMenu which $ \(idx, _) ->
        handleMenuEvent (which.menuBarMenusL.ix idx) e

    when (not handled) $
        which %= openPreviousMenu

    return True
handleMenuBarEvent which e@(VtyEvent (Vty.EvKey Vty.KRight [])) = do
    -- Since this key might be handled by the open menu, try that first
    -- and only switch menus if it wasn't handled by the menu.
    handled <- withOpenMenu which $ \(idx, _) ->
        handleMenuEvent (which.menuBarMenusL.ix idx) e

    when (not handled) $
        which %= openNextMenu

    return True
handleMenuBarEvent which e@(MouseDown n _ _ _) = do
    mb <- use which
    case getMenuTitleMatch mb n of
        Nothing -> withOpenMenu which $ \(idx, _) ->
            handleMenuEvent (which.menuBarMenusL.ix idx) e
        Just (i, _) -> do
            mMatchingMenu <- preuse (which.menuBarMenusL.ix i)
            case mMatchingMenu of
                Nothing -> return ()
                Just matchingMenu ->
                    when (not $ menuIsOpen matchingMenu) $ do
                        which %= closeAllMenus
                        which %= openMenuAtIndex i
            return True
handleMenuBarEvent which e =
    withOpenMenu which $ \(idx, _) ->
        handleMenuEvent (which.menuBarMenusL.ix idx) e

-- | Given a menu bar with an open menu, switch the open menu to the one
-- preceding the currently open one, or do nothing if no menu is open.
openPreviousMenu :: MenuBar s n k -> MenuBar s n k
openPreviousMenu mb = fromMaybe mb $ do
    (i, _) <- getOpenMenu mb
    let newIndex = if i == 0
                   then V.length (mb^.menuBarMenusL) - 1
                   else i - 1
    return $ openMenuAtIndex newIndex $ closeAllMenus mb

-- | Given a menu bar with an open menu, switch the open menu to the one
-- following the currently open one, or do nothing if no menu is open.
openNextMenu :: MenuBar s n k -> MenuBar s n k
openNextMenu mb = fromMaybe mb $ do
    (i, _) <- getOpenMenu mb
    let newIndex = if i == V.length (mb^.menuBarMenusL) - 1
                   then 0
                   else i + 1
    return $ openMenuAtIndex newIndex mb

-- | Close all open menus in this menu bar.
closeAllMenus :: MenuBar s n k -> MenuBar s n k
closeAllMenus mb = mb & menuBarMenusL.each %~ closeMenu

-- | Open the menu at the specified index, closing any other open menus
-- in the menu bar. If the index is invalid, this does nothing.
openMenuAtIndex :: Int -> MenuBar s n k -> MenuBar s n k
openMenuAtIndex i mb = (closeAllMenus mb) & menuBarMenusL.ix i %~ openMenu

-- | Set the menu orientation of the menu bar and all of its menus. For
-- details, see 'setMenuOrientation'.
setMenuBarOrientation :: MenuOrientation -> MenuBar s n k -> MenuBar s n k
setMenuBarOrientation o mb = mb & menuBarOrientationL .~ o
                                & menuBarMenusL.each %~ setMenuOrientation o

-- | Toggle the open state of the menu at the specified index. If
-- toggling to open, this will close any other open menus in the menu
-- bar. If the index is invalid, this does nothing.
toggleMenuAtIndex :: Int -> MenuBar s n k -> MenuBar s n k
toggleMenuAtIndex i mb =
    case getOpenMenu mb of
        Nothing -> openMenuAtIndex i mb
        Just (idx, _) -> if idx == i
                         then closeAllMenus mb
                         else openMenuAtIndex i mb

-- | Given a lens to access a menu bar and a handler to invoke on its
-- currently open menu, invoke the handler if there is an open menu and
-- return its result, or do nothing and return False otherwise.
withOpenMenu :: Lens' s (MenuBar s n k) -> ((Int, Menu s n k) -> EventM n s Bool) -> EventM n s Bool
withOpenMenu which f = do
    mb <- use which
    case getOpenMenu mb of
        Nothing -> return False
        Just pair -> f pair