{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE TemplateHaskell #-} {-| Module : Brick.Widgets.DropDownMenu Description : A drop-down menu type and functions for manipulating and rendering it. Copyright : (c) Mario Lang, 2018 License : BSD-style (see the file LICENSE) Maintainer : mlang@blind.guru Stability : experimental Portability : POSIX This module provides a simple drop-down menu widget with global key bindings. A submenu consists of a title, a resource name, and an associated global event. Each menu item consists of the items title, resource name, global event and associated action. An action is a function @s -> 'EventM' n ('Next' s)@. -} module Brick.Widgets.DropDownMenu ( DropDownMenu -- * Constructing a drop-down menu , dropDownMenu -- * Handling events , handleDropDownMenuEvent, handleGlobalDropDownMenuEvent -- * Rendering drop-down menus , renderDropDownMenu -- * Accessors , isDropDownMenuOpen -- * Manipulating a drop-down menu , closeDropDownMenu -- * Attributes , menuAttr, menuSelectedAttr ) where import Brick ( Named(..) , AttrName , EventM, Next , Location(..), Widget , continue, emptyWidget , handleEventLensed , hBox, hLimit , padLeftRight, padTopBottom , showCursor, str , textWidth, vLimit , withAttr ) import Brick.Widgets.Border ( borderWithLabel ) import Brick.Widgets.List ( List, list , handleListEvent , listElements, listMoveTo , listSelectedElement , renderList ) import Data.Foldable ( toList ) import Data.List ( find, findIndex , intersperse ) import Data.List.PointedList ( PointedList , focus, moveTo, withFocus ) import Data.List.PointedList.Circular ( next, previous ) import qualified Data.List.PointedList as PointedList ( fromList ) import Data.Map ( Map ) import qualified Data.Map as Map ( empty, fromList ) import Data.Maybe ( fromMaybe, mapMaybe ) import qualified Data.Vector as Vector ( fromList, length ) import Graphics.Vty ( Event(..) , Key(..), Modifier(..) ) import Lens.Micro.GHC ( Lens', LensLike' , _2, _Just , (&), (^.), (^?) , (.~), (%~), at, set ) import Lens.Micro.TH ( makeLenses ) type MenuItem s n = (String, n, Maybe Event, s -> EventM n (Next s)) type Menu s n = [(String, n, Maybe Event, [MenuItem s n])] data Action s n = MoveTo !Int | Invoke (s -> EventM n (Next s)) -- | Drop-down menus present a menu bar with drop-down submenus. -- -- Drop-down menus support the following events by default: -- -- * Left/right arrow keys: Switch to previous/next submenu -- * Up arrow key: Close submenu when already at top, otherwise move selection in submenu -- * Down arrow key: Open submenu or move submenu selection downwards -- * Escape: Close submenu -- * Return: Open submenu or invoke selected submenu item data DropDownMenu s n = DropDownMenu { _menuName :: n , _menuOpen :: Bool , _menuKeyMap :: Map Event (Action s n) , _menuList :: Maybe (PointedList (String, List n (MenuItem s n))) } makeLenses ''DropDownMenu submenuList :: Applicative f => LensLike' f (DropDownMenu s n) (List n (MenuItem s n)) submenuList = menuList . _Just . focus . _2 instance Named (DropDownMenu s n) n where getName = _menuName dropDownMenu :: n -- ^ The resource name for this drop-down menu -> [(String, n, Maybe Event, [(String, n, Maybe Event, s -> EventM n (Next s))])] -- ^ Menu description -> DropDownMenu s n dropDownMenu name desc = DropDownMenu name False keyMap $ PointedList.fromList $ (\(t, n, _, c) -> (t, list n (Vector.fromList c) 1)) <$> desc where keyMap = Map.fromList . concat $ zipWith f [0..] desc where f i (_, _, Just e, xs) = (e, MoveTo i) : mapMaybe g xs f _ (_, _, Nothing, xs) = mapMaybe g xs g (_, _, Just e, a) = Just (e, Invoke a) g (_, _, Nothing, _) = Nothing -- | Handle drop-down menu events. -- This should typically be called from the application event handler -- if this menu widget has focus. handleDropDownMenuEvent :: (Eq n, Ord n) => s -- ^ The application state -> Lens' s (DropDownMenu s n) -- ^ A lens for accessing the drop-down menu state -> (s -> s) -- ^ Sets focus to this drop-down menu widget if need be -> Event -- ^ Event received from Vty -> EventM n (Next s) handleDropDownMenuEvent s target setFocus = \case EvKey KLeft [] -> continue $ s & target.menuList._Just %~ previous EvKey KRight [] -> continue $ s & target.menuList._Just %~ next EvKey KEsc [] -> continue $ s & target.menuOpen .~ False EvKey KUp [] | s^.target.menuOpen && fmap fst (listSelectedElement =<< s ^? target.submenuList) == Just 0 -> continue $ s & target.menuOpen .~ False EvKey KDown [] | not $ s^.target.menuOpen -> continue $ s & target.menuOpen .~ True & target.submenuList %~ listMoveTo 0 EvKey KEnter [] | not $ s^.target.menuOpen -> continue $ s & target.menuOpen .~ True | otherwise -> case fmap snd (listSelectedElement =<< s ^? target.submenuList) of Nothing -> continue s Just (_, _, _, f) -> f s e | s^.target.menuOpen -> fromMaybe (continue =<< handleEventLensed s target handleSubmenuEvent e) $ handleGlobalDropDownMenuEvent s target setFocus e | otherwise -> fromMaybe (continue s) $ handleGlobalDropDownMenuEvent s target setFocus e -- | Handle global events. -- This function will handle global events associated with submenus -- or menu items. It should typically be called from the main -- application event handler before any other more specific handlers. handleGlobalDropDownMenuEvent :: (Eq n, Ord n) => s -- ^ The application state -> Lens' s (DropDownMenu s n) -- ^ A lens for accessing the drop-down menu state -> (s -> s) -- ^ Set application focus -> Event -- ^ Event received from Vty -> Maybe (EventM n (Next s)) handleGlobalDropDownMenuEvent s target setFocus e = go =<< s ^. target . menuKeyMap . at e where go (MoveTo n) = case moveTo n =<< s ^. target.menuList of Nothing -> Nothing l -> pure . continue . setFocus $ s & target.menuList .~ l & target.menuOpen .~ True go (Invoke f) = pure $ f s renderDropDownMenu :: (Eq n, Ord n, Show n) => Bool -- ^ Does this menu have focus? -> DropDownMenu s n -> Widget n renderDropDownMenu focused m = case m^?menuList._Just of Nothing -> emptyWidget Just menus -> let o = m^.menuOpen f ((t, l), sel) = let cursor = if focused && sel && not o then showCursor (getName l) (Location (0, 0)) else id attr = if focused && sel && not o then withAttr menuSelectedAttr else id height = Vector.length $ listElements l width = maximum $ textWidth . showMenuItem <$> listElements l in if focused && sel && o then borderWithLabel (str t) $ padLeftRight 1 $ vLimit height $ hLimit width $ renderList drawMenuItem focused l else attr $ cursor $ str t in hBox $ intersperse (str " ") $ map f (toList (withFocus menus)) handleSubmenuEvent :: (Ord n) => Event -> DropDownMenu s n -> EventM n (DropDownMenu s n) handleSubmenuEvent e m = maybe (pure m) handleEvent $ m ^? menuList._Just where handleEvent menus = do menus' <- handleEventLensed menus (focus._2) handleListEvent e pure $ m & menuList._Just .~ menus' drawMenuItem :: Bool -> MenuItem s n -> Widget n drawMenuItem sel x@(t, n, e, _) = let cursor = if sel then showCursor n (Location (0, 0)) else id in cursor $ str $ showMenuItem x showMenuItem :: MenuItem s n -> String showMenuItem (t, _, Just e, _) = t <> " (" <> showEvent e <> ")" showMenuItem (t, _, Nothing, _) = t showEvent :: Event -> String showEvent (EvKey (KChar c) mods) = concatMap showModifier mods <> [c] where showModifier MCtrl = "Ctrl-" showModifier MMeta = "Meta-" showModifier MAlt = "Alt-" showModifier _ = "" isDropDownMenuOpen :: DropDownMenu s n -> Bool isDropDownMenuOpen = (^. menuOpen) -- | Close submenu. closeDropDownMenu :: DropDownMenu s n -> DropDownMenu s n closeDropDownMenu = set menuOpen False menuAttr, menuSelectedAttr :: AttrName menuAttr = "dropdownmenu" menuSelectedAttr = menuAttr <> "selected"