packages feed

ghcup-0.2.1.0: lib-tui/GHCup/Brick/Widgets/Menu.hs

{-# LANGUAGE CPP               #-}
{-# LANGUAGE DataKinds         #-}
{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications  #-}
{-# LANGUAGE RankNTypes        #-}
{-# LANGUAGE ViewPatterns      #-}
{-# OPTIONS_GHC -Wno-unused-record-wildcards #-}
{-# OPTIONS_GHC -Wno-unused-matches #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell   #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE GADTs #-}


{- **************

A general system inspired by Brick.Form. It uses optics instead of microlenses and it is less generic than
Brick.Form, but generic enough to serve our purpose.

A Menu consists in
  a) A state value
  b) A list of fields. Each field is capable of modifying a part of the state
  c) some metadata

A field (type MenuField) consists in
  a) a Lens to a part of the Menu state, so the Menu can call that lens to modify its own state
  b) an input widget

An input (type FieldInput) consist in
  a) some state
  b) a validator function
  c) a handler and a renderer

We have to use existential types to achieve a composable API since every FieldInput has a different
internal type, and every MenuField has a different Lens. For example:
  - The menu state is a record (MyRecord {uri: URI, flag : Bool})
  - Then, there are two MenuField:
    - One MenuField has (Lens' MyRecord URI) and the other has (Lens' MyRecord Bool)
    - The MenuFields has FieldInputs with internal state Text and Bool, respectively
  - Obviously, the MenuField has to be polymorphic in the Lens' and in the Input internal state,
    But we must hide that polymorphism (existential), in order to store all MenuField in a List

************** -}

module GHCup.Brick.Widgets.Menu where

import qualified GHCup.Brick.Attributes as Attributes
import qualified GHCup.Brick.Common as Common

import Brick
    ( BrickEvent(..),
      EventM,
      Widget(..),
      (<+>))
import qualified Brick
import qualified Brick.Widgets.Border as Border
import qualified Brick.Widgets.Border.Style as Border
import qualified Brick.Widgets.Center as Brick
import qualified Brick.Widgets.List as L
import qualified Brick.Widgets.Edit as Edit
import           Brick.Focus (FocusRing)
import qualified Brick.Focus as F
import           Data.Function ( (&))
import           Prelude             hiding ( appendFile )

import           Data.Maybe
import qualified Data.Text                     as T


import           Optics.TH (makeLensesFor)
import qualified Graphics.Vty as Vty
import Optics.State.Operators ((%=), (.=))
import Optics.Optic ((%))
import Optics.State (use, assign)
import GHCup.Types (KeyCombination(..))
import Optics (Lens', to, lens, _1, over)
import Optics.Operators ( (^.), (.~) )
import Data.Foldable (find, foldl')
import           Data.List.NonEmpty             ( NonEmpty (..) )
import qualified Data.List.NonEmpty            as NE


-- | Just some type synonym to make things explicit
type Formatter n = Bool -> Widget n -> Widget n
-- | A label
type Label = T.Text
-- | A help message of an entry
type HelpMessage = T.Text
-- | A button name
type ButtonName n = n

idFormatter :: Formatter n
idFormatter = const id


-- | An error message
type ErrorMessage = T.Text
data ErrorStatus = Valid | Invalid ErrorMessage deriving (Eq)

-- | A lens which does nothing. Useful to define no-op fields
emptyLens :: Lens' s ()
emptyLens = lens (const ()) (\s _ -> s)

-- | A FieldInput is a pair label-content
--   a - is the type of the field it manipulates
--   b - is its internal state (modified in the gui)
--   n - your application's resource name type
data FieldInput a b n =
  FieldInput
    { inputState :: b                                     -- ^ The state of the input field (what's rendered in the screen)
    , inputValidator :: b -> Either ErrorMessage a        -- ^ A validator function
    , inputHelp   :: HelpMessage                          -- ^ The input helpMessage
    , inputRender :: Bool
                  -> ErrorStatus
                  -> HelpMessage
                  -> Label
                  -> b
                  -> (Widget n -> Widget n)
                  -> (Widget n, Maybe (Widget n))         -- ^ How to draw the input and optionally an overlay, with focus a help message and input.
                                                          --   A extension function can be applied too
    , inputHandler :: BrickEvent n () -> EventM n b ()    -- ^ The handler
    }

makeLensesFor
  [ ("inputState", "inputStateL")
  , ("inputValidator", "inputValidatorL")
  , ("inputName", "inputNameL")
  , ("inputHelp", "inputHelpL")
  ]
  ''FieldInput

-- | The MenuField is an existential type which stores a Lens' to a part of the Menu state.
--   In also contains a Field input which internal state is hidden
data MenuField s n where
  MenuField ::
      { fieldAccesor   :: Lens' s a                   -- ^ A Lens pointing to some part of the state
      , fieldInput     :: FieldInput a b n            -- ^ The input which modifies the state
      , fieldLabel     :: Label                       -- ^ The label
      , fieldStatus    :: ErrorStatus                 -- ^ Whether the current is valid or not.
      , fieldName      :: n
      } -> MenuField s n

isValidField :: MenuField s n -> Bool
isValidField = (== Valid) . fieldStatus

makeLensesFor
  [ ("fieldLabel", "fieldLabelL")
  , ("fieldStatus", "fieldStatusL")
  ]
  ''MenuField

data SelectState i n = SelectState
  { selectStateItems :: (NonEmpty (Int, (i, Bool)), Bool) -- ^ All items along with their selected state
                                                          -- And Bool to indicate if editable field is selected
  , selectStateEditState :: Maybe (Edit.Editor T.Text n)  -- ^ Editable field's editor state
  , selectStateFocusRing :: FocusRing Int                 -- ^ Focus ring using integral values assigned to each item
  , selectStateOverlayOpen :: Bool                        -- ^ Whether the select menu is open
  }

makeLensesFor
  [ ("selectStateItems", "selectStateItemsL")
  , ("selectStateEditState", "selectStateEditStateL")
  , ("selectStateFocusRing", "selectStateFocusRingL")
  , ("selectStateOverlayOpen", "selectStateOverlayOpenL")
  ]
  ''SelectState

data EditState n = EditState
  { editState :: Edit.Editor T.Text n
  , editStateOverlayOpen :: Bool                        -- ^ Whether the edit menu is open
  }

makeLensesFor
  [ ("editState", "editStateL")
  , ("editStateOverlayOpen", "editStateOverlayOpenL")
  ]
  ''EditState

data MenuKeyBindings = MenuKeyBindings
  { mKbUp              :: KeyCombination
  , mKbDown            :: KeyCombination
  , mKbQuit            :: KeyCombination
  }
  deriving (Show)

makeLensesFor
  [ ("mKbUp", "mKbUpL")
  , ("mKbDown", "mKbDownL")
  , ("mKbQuit", "mKbQuitL")
  ]
  ''MenuKeyBindings

-- | A fancy lens to the help message
fieldHelpMsgL :: Lens' (MenuField s n) HelpMessage
fieldHelpMsgL = lens g s
  where g (MenuField {..})= fieldInput ^. inputHelpL
        s (MenuField{..}) msg = MenuField {fieldInput = fieldInput & inputHelpL .~ msg , ..}

-- | How to draw a field given a formatter
drawField :: Formatter n -> Bool -> MenuField s n -> Widget n
drawField amp focus (MenuField { fieldInput = FieldInput {..}, ..}) =
  let (input, overlay) = inputRender focus fieldStatus inputHelp fieldLabel inputState (amp focus)
    in case (focus, overlay) of
         (True, Nothing) -> Common.enableScreenReader fieldName $ Brick.visible input
         _ -> input

drawFieldOverlay :: MenuField s n -> Maybe (Widget n)
drawFieldOverlay (MenuField { fieldInput = FieldInput {..}, ..}) =
  snd $ inputRender True fieldStatus inputHelp fieldLabel inputState id

instance Brick.Named (MenuField s n) n where
  getName :: MenuField s n -> n
  getName entry = entry & fieldName


{- *****************
  CheckBox widget
***************** -}

type CheckBoxField = MenuField

createCheckBoxInput :: FieldInput Bool Bool n
createCheckBoxInput = FieldInput False Right "" checkBoxRender checkBoxHandler
  where
    border w = Brick.txt "[" <+> Brick.padRight (Brick.Pad 1) (Brick.padLeft (Brick.Pad 2) w) <+> Brick.txt "]"
    drawBool b =
        if b
          then border . Brick.withAttr Attributes.installedAttr    $ Brick.str Common.checkBoxSelectedSign
          else border . Brick.withAttr Attributes.notInstalledAttr $ Brick.str Common.notInstalledSign
    checkBoxRender focus _ help _ check f = (, Nothing) $
      let core = f $ drawBool check
      in if focus
        then core
        else core <+> (Brick.padLeft (Brick.Pad 1) . renderAsHelpMsg $ help)
    checkBoxHandler = \case
        VtyEvent (Vty.EvKey Vty.KEnter []) -> Brick.modify not
        _ -> pure ()

createCheckBoxField :: n -> Lens' s Bool -> CheckBoxField s n
createCheckBoxField name access = MenuField access createCheckBoxInput "" Valid name

{- *****************
  Editable widget
***************** -}

type EditableField = MenuField

createEditableInput :: (Ord n, Show n) => T.Text -> n -> (T.Text -> Either ErrorMessage a) -> FieldInput a (EditState n) n
createEditableInput initText name validator = FieldInput initEdit validateEditContent "" drawEdit handler
  where
    drawEdit focus errMsg help label (EditState edi overlayOpen) amp = (field, mOverlay)
      where
        field =
          let
            borderBox w = amp (Brick.vLimit 1 $ Border.vBorder <+> Brick.padRight Brick.Max w <+> Border.vBorder)
            editorContents = Brick.txt $ T.unlines $ Edit.getEditContents edi
            isEditorEmpty = Edit.getEditContents edi == [mempty]
                          || Edit.getEditContents edi == [initText]

          in case errMsg of
               Valid | isEditorEmpty -> borderBox $ renderAsHelpMsg help
                     | otherwise -> borderBox editorContents
               Invalid msg
                 | focus && isEditorEmpty -> borderBox $ renderAsHelpMsg help
                 | focus     -> borderBox editorContents
                 | otherwise -> borderBox $ renderAsErrMsg msg
        mOverlay = if overlayOpen
          then Just (overlayLayer ("Edit " <> label) overlay)
          else Nothing
        overlay = Brick.vBox
          [ Brick.txtWrap help
          , Border.border $ Edit.renderEditor (Brick.txt . T.unlines) focus edi
          , case errMsg of
              Invalid msg -> renderAsErrMsg msg
              _ -> Brick.txt " "
          , Brick.padRight Brick.Max $
              Brick.txt "Press Enter to go back"
          ]
    handler ev = do
      (EditState edi overlayOpen) <- Brick.get
      if overlayOpen
        then case ev of
          VtyEvent (Vty.EvKey Vty.KEnter []) -> editStateOverlayOpenL .= False
          _ -> Common.zoom editStateL $ Edit.handleEditorEvent ev
        else case ev of
          VtyEvent (Vty.EvKey Vty.KEnter []) -> editStateOverlayOpenL .= True
          _ -> pure ()
    validateEditContent = validator . T.init . T.unlines . Edit.getEditContents . editState
    initEdit = EditState (Edit.editorText name (Just 1) initText) False

createEditableField' :: (Eq n, Ord n, Show n) => T.Text -> n -> (T.Text -> Either ErrorMessage a) -> Lens' s a -> EditableField s n
createEditableField' initText name validator access = MenuField access input "" Valid name
  where
    input = createEditableInput initText name validator

createEditableField :: (Eq n, Ord n, Show n) => n -> (T.Text -> Either ErrorMessage a) -> Lens' s a -> EditableField s n
createEditableField = createEditableField' ""

{- *****************
  Button widget
***************** -}

type Button = MenuField

createButtonInput :: FieldInput () () n
createButtonInput = FieldInput () Right "" drawButton (const $ pure ())
  where
    drawButton True (Invalid err) _    _ _ amp = (amp . renderAsErrMsg $ err, Nothing)
    drawButton _    _             help _ _ amp =
      let pad = if length (T.lines help) == 1 then Brick.padTop (Brick.Pad 1) else id
      in (amp . pad . renderAsHelpMsg $ help, Nothing)

createButtonField :: n -> Button s n
createButtonField = MenuField emptyLens createButtonInput "" Valid

{- *****************
  Select widget
***************** -}

type SelectField = MenuField

createSelectInput :: (Ord n, Show n)
  => NonEmpty i
  -> (i -> T.Text)
  -> (Int -> (NonEmpty (Int, (i, Bool)), Bool) -> (NonEmpty (Int, (i, Bool)), Bool))
  -> (([i], Maybe T.Text) -> Either ErrorMessage k)
  -> n
  -> Maybe n
  -> MenuKeyBindings
  -> FieldInput k (SelectState i n) n
createSelectInput items showItem updateSelection validator viewportFieldName mEditFieldName kb
  = FieldInput initState (validator . getSelectedItems) "" selectRender selectHandler
  where
    totalRows = (if isJust mEditFieldName then (+) 1 else id) $ length items
    initState = SelectState
      (NE.zip (1 NE.:| [2..]) $ fmap (,False) items, False)
      ((\n -> Edit.editorText n (Just 1) "") <$> mEditFieldName)
      (F.focusRing [1.. totalRows])
      False
    getSelectedItems (SelectState {..}) =
      ( fmap (fst . snd) . filter (snd . snd) . NE.toList . fst $ selectStateItems
      , if snd selectStateItems then T.init . T.unlines . Edit.getEditContents <$> selectStateEditState else Nothing)

    border w = Brick.txt "[" <+> Brick.padRight (Brick.Pad 1) (Brick.padLeft (Brick.Pad 1) w) <+> Brick.txt "]"
    selectRender focus errMsg help label s amp = (field, mOverlay)
      where
        field =
          let mContents = case getSelectedItems s of
                ([], Nothing) -> Nothing
                (xs, mTxt) -> Just $ fmap (Brick.padRight (Brick.Pad 1) . Brick.txt . showItem) xs
                   ++ (case mTxt of Just t -> [Brick.txt t]; Nothing -> [])
          in amp $ case (errMsg, mContents) of
            (Valid, Nothing) -> Brick.padLeft (Brick.Pad 1) . renderAsHelpMsg $ help
            (Valid, Just contents) -> border $ Brick.hBox contents
            (Invalid msg, Nothing)
              | focus -> Brick.padLeft (Brick.Pad 1) . renderAsHelpMsg $ help
              | otherwise -> Brick.padLeft (Brick.Pad 1) $ renderAsErrMsg msg
            (Invalid msg, Just contents)
              | focus -> border $ Brick.hBox contents
              | otherwise -> Brick.padLeft (Brick.Pad 1) $ renderAsErrMsg msg

        mOverlay = if selectStateOverlayOpen s
          then Just (overlayLayer ("Select " <> label)  $ overlay s errMsg help)
          else Nothing
    overlay (SelectState {..}) errMsg help = Brick.vBox
      [ if txtFieldFocused
          then Brick.txtWrap "Press Enter to finish editing and select custom value. Press Up/Down keys to navigate"
          else Brick.txt "Press "
            <+> Common.keyToWidget (kb ^. mKbQuitL)
            <+> Brick.txt " to go back, Press Enter to select"
      , case errMsg of Invalid msg -> renderAsErrMsg msg; _ -> Brick.emptyWidget
      , Brick.vLimit totalRows $ Brick.withVScrollBars Brick.OnRight
          $ Brick.viewport viewportFieldName Brick.Vertical
          $ Brick.vBox $ mEditableField ++ NE.toList (fmap (mkSelectRow focused) (fst selectStateItems))
      ]
      where focused = fromMaybe 1 $ F.focusGetCurrent selectStateFocusRing
            txtFieldFocused = focused == totalRows
            mEditableField = case selectStateEditState of
              Just edi -> [ mkEditTextRow txtFieldFocused edi (snd selectStateItems) help ]
              Nothing -> []

    mkSelectRow focused (ix, (item, selected)) = (if focused == ix then Brick.visible else id) $
      Brick.txt "[" <+> Brick.padRight (Brick.Pad 1) (Brick.padLeft (Brick.Pad 1) m) <+> Brick.txt "] "
        <+> renderAslabel (showItem item) (focused == ix)
      where m = if selected then Brick.txt "*" else Brick.txt " "

    mkEditTextRow focused edi selected help = (if focused then Brick.visible else id) $
      Brick.txt "[" <+> Brick.padRight (Brick.Pad 1) (Brick.padLeft (Brick.Pad 1) m) <+> Brick.txt "] "
        <+> if not focused && Edit.getEditContents edi == [mempty]
               then Brick.txt "(Specify custom text value)"
               else Brick.vLimit 1 $ Border.vBorder <+> Brick.padRight Brick.Max (Edit.renderEditor (Brick.txt . T.unlines) focused edi) <+> Border.vBorder
      where m = if selected then Brick.txt "*" else Brick.txt " "

    selectHandler ev = do
      s <- Brick.get
      if selectStateOverlayOpen s
        then do
          focused <- use (selectStateFocusRingL % to F.focusGetCurrent)
          mEditState <- use selectStateEditStateL
          case (focused, mEditState) of
            (Just ix, Just edi)
              | ix == totalRows -> case ev of
              VtyEvent (Vty.EvKey Vty.KEnter []) -> do
                selectStateItemsL %= updateSelection ix
                selectStateFocusRingL %= F.focusNext
              VtyEvent (Vty.EvKey Vty.KDown [])         -> selectStateFocusRingL %= F.focusNext
              VtyEvent (Vty.EvKey Vty.KUp [])           -> selectStateFocusRingL %= F.focusPrev
              _ -> do
                newEdi <- Brick.nestEventM' edi $ Edit.handleEditorEvent ev
                assign selectStateEditStateL (Just newEdi)
                selectStateItemsL %= updateSelection ix
            _ -> case ev of
              VtyEvent (Vty.EvKey k m)
                | KeyCombination k m == kb ^. mKbQuitL -> selectStateOverlayOpenL .= False
                | KeyCombination k m == kb ^. mKbUpL   -> selectStateFocusRingL %= F.focusPrev
                | KeyCombination k m == kb ^. mKbDownL -> selectStateFocusRingL %= F.focusNext
              VtyEvent (Vty.EvKey Vty.KEnter [])        -> do
                selectStateItemsL %= updateSelection (fromMaybe 1 focused)
              _ -> pure ()
        else case ev of
          VtyEvent (Vty.EvKey Vty.KEnter []) -> selectStateOverlayOpenL .= True
          _ -> pure ()

-- | Select Field with only single selection possible, aka radio button
createSelectField :: (Ord n, Show n) => n -> Lens' s (Maybe i) -> NonEmpty i -> (i -> T.Text) -> MenuKeyBindings -> SelectField s n
createSelectField name access items showItem keyBindings = MenuField access (createSelectInput items showItem singleSelect getSelection name Nothing keyBindings) "" Valid name
  where
    singleSelect :: Int -> (NonEmpty (Int, (i, Bool)), a) -> (NonEmpty (Int, (i, Bool)), a)
    singleSelect ix = over _1 $ fmap (\(ix', (i, b)) -> if ix' == ix then (ix', (i, True)) else (ix', (i, False)))

    getSelection = Right . fmap NE.head . NE.nonEmpty . fst

-- | Select Field with multiple selections possible
createMultiSelectField :: (Ord n, Show n) => n -> Lens' s [i] -> NonEmpty i -> (i -> T.Text) -> MenuKeyBindings -> SelectField s n
createMultiSelectField name access items showItem keyBindings = MenuField access (createSelectInput items showItem multiSelect (Right . fst) name Nothing keyBindings) "" Valid name
  where
    multiSelect :: Int -> (NonEmpty (Int, (i, Bool)), a) -> (NonEmpty (Int, (i, Bool)), a)
    multiSelect ix = over _1 $ fmap (\(ix', (i, b)) -> if ix' == ix then (ix', (i, not b)) else (ix', (i, b)))

-- | Select Field with only single selection possible, along with an editable field
createSelectFieldWithEditable :: (Ord n, Show n) => n -> n -> Lens' s (Either a i) -> (T.Text -> Either ErrorMessage a) -> NonEmpty i -> (i -> T.Text) -> MenuKeyBindings -> SelectField s n
createSelectFieldWithEditable name editFieldName access validator items showItem keyBindings = MenuField access (createSelectInput items showItem singleSelect getSelection name (Just editFieldName) keyBindings) "" Valid name
  where
    singleSelect :: Int -> (NonEmpty (Int, (i, Bool)), Bool) -> (NonEmpty (Int, (i, Bool)), Bool)
    singleSelect ix (ne, a) = (fmap (\(ix', (i, b)) -> if ix' == ix then (ix', (i, True)) else (ix', (i, False))) ne, ix == length ne + 1)

    getSelection (_, Just txt) = Left <$> validator txt
    getSelection (ls, _) = maybe (Left <$> validator "") (Right . Right . NE.head) $ NE.nonEmpty ls


{- *****************
  Utilities
***************** -}

-- | highlights a widget (using List.listSelectedFocusedAttr)
highlighted :: Widget n -> Widget n
highlighted = Brick.withAttr L.listSelectedFocusedAttr

-- | Given a text, crates a highlighted label on focus. An amplifier can be passed
renderAslabel :: T.Text -> Bool -> Widget n
renderAslabel t focus =
  if focus
    then highlighted $ Brick.txt t
    else Brick.txt t

-- | Creates a left align column.
-- Example:       |- col2 is align despite the length of col1
--   row1_col1         row1_col2
--   row2_col1_large   row2_col2
leftify :: Int -> Brick.Widget n -> Brick.Widget n
leftify i = Brick.hLimit i . Brick.padRight Brick.Max

-- | Creates a right align column.
-- Example:       |- col2 is align despite the length of col1
--         row1_col1   row1_col2
--   row2_col1_large   row2_col2
rightify :: Int -> Brick.Widget n -> Brick.Widget n
rightify i = Brick.hLimit i . Brick.padLeft Brick.Max

-- | render some Text using helpMsgAttr
renderAsHelpMsg :: T.Text -> Widget n
renderAsHelpMsg = Brick.withAttr Attributes.helpMsgAttr . Brick.txt

-- | render some Text using errMsgAttr
renderAsErrMsg :: T.Text -> Widget n
renderAsErrMsg = Brick.withAttr Attributes.errMsgAttr . Brick.txt

-- | Used to create a layer on top of menu
overlayLayer :: T.Text -> Brick.Widget n -> Brick.Widget n
overlayLayer layer_name =
    Brick.centerLayer
      . Brick.hLimitPercent 50
      . Brick.vLimitPercent 65
      . Brick.withBorderStyle Border.unicode
      . Border.borderWithLabel (Brick.txt layer_name)

{- *****************
  Menu widget
***************** -}

-- | A menu is a list of Fields and a state. Informally we can think about s in terms of the record type returned by
-- a form.
data Menu s n
    = Menu
    { menuFields    :: [MenuField s n]   -- ^ The datatype representing the list of entries. Precisely, any array-like data type is highly unconvinient.
    , menuState     :: s
    , menuValidator :: s -> Maybe ErrorMessage  -- ^ A validator function
    , menuButtons   :: [Button s n]      -- ^ The buttons. Commonly, the handlers for buttons are defined outside the menu handler.
    , menuFocusRing :: FocusRing n       -- ^ The focus ring with the resource name for each entry and each button, in the order you want to loop them.
    , menuKeyBindings :: MenuKeyBindings   -- ^ KeyBindings for navigation
    , menuName      :: n                 -- ^ The resource Name.
    , menuTitle     :: T.Text            -- ^ Menu title.
    }

makeLensesFor
  [ ("menuFields", "menuFieldsL"), ("menuState", "menuStateL"), ("menuValidator", "menuValidatorL")
  , ("menuButtons", "menuButtonsL"), ("menuFocusRing", "menuFocusRingL")
  , ("menuKeyBindings", "menuKeyBindingsL"), ("menuName", "menuNameL")
  , ("menuTitle", "menuTitleL")
  ]
  ''Menu

isValidMenu :: Menu s n -> Bool
isValidMenu m = all isValidField (menuFields m)
  && (case menuValidator m (menuState m) of { Nothing -> True; _ -> False })

createMenu ::
     n
  -> s
  -> T.Text
  -> (s -> Maybe ErrorMessage)
  -> MenuKeyBindings
  -> [Button s n]
  -> [MenuField s n]
  -> Menu s n
createMenu n initial title validator keys buttons fields = Menu fields initial validator buttons ring keys n title
  where ring = F.focusRing $ [field & fieldName | field <- fields] ++ [button & fieldName | button <- buttons]

handlerMenu :: forall n e s. Eq n => BrickEvent n e -> EventM n (Menu s n) ()
handlerMenu ev = do
  fields  <- use menuFieldsL
  kb  <- use menuKeyBindingsL
  focused <- use $ menuFocusRingL % to F.focusGetCurrent
  let focusedField = (\n -> find (\x -> Brick.getName x == n) fields) =<< focused
      propagateEvent e = case focused of
        Nothing -> pure ()
        Just n  -> do
          updated_fields <- updateFields n (VtyEvent e) fields
          validator  <- use menuValidatorL
          state <- use menuStateL
          if all isValidField updated_fields
            then case validator state of
              Nothing -> menuButtonsL %= fmap (fieldStatusL .~ Valid)
              Just err -> menuButtonsL %= fmap (fieldStatusL .~ Invalid err)
            else menuButtonsL %= fmap (fieldStatusL .~ Invalid "Some fields are invalid")
          menuFieldsL .= updated_fields
  case drawFieldOverlay =<< focusedField of
    Just _ -> case ev of
      VtyEvent e -> propagateEvent e
      _ -> pure ()
    Nothing -> case ev of
      VtyEvent (Vty.EvKey k m)
        | KeyCombination k m == kb ^. mKbUpL    -> menuFocusRingL %= F.focusPrev
        | KeyCombination k m == kb ^. mKbDownL  -> menuFocusRingL %= F.focusNext
      VtyEvent e -> propagateEvent e
      _ -> pure ()
 where
  -- runs the Event with the inner handler of MenuField.
  updateFields :: n -> BrickEvent n () -> [MenuField s n] -> EventM n (Menu s n) [MenuField s n]
  updateFields n e = traverse $ \x@(MenuField {fieldInput = FieldInput {..}, ..}) ->
    if Brick.getName x == n
      then do
       newb <- Brick.nestEventM' inputState (inputHandler e)
       let newField = MenuField {fieldInput = (FieldInput {inputState=newb, ..}) , ..}
       case inputValidator newb of
        Left errmsg -> pure $ newField & fieldStatusL .~ Invalid errmsg
        Right a     -> menuStateL % fieldAccesor .= a >> pure (newField & fieldStatusL .~ Valid)
      else pure x


drawMenu :: (Eq n, Ord n, Show n, Brick.Named (MenuField s n) n) => Menu s n -> [Widget n]
drawMenu menu =
  overlays ++
  [Common.frontwardLayer (menu ^. menuTitleL) mainLayer]
  where
    mainLayer = Brick.vBox
      [ Brick.vBox buttonWidgets
      , Common.separator
      , Brick.vLimit (length fieldLabels) $ Brick.withVScrollBars Brick.OnRight
          $ Brick.viewport (menu ^. menuNameL) Brick.Vertical
          $ Brick.vBox fieldWidgets
      , Brick.txt " "
      , Brick.padRight Brick.Max $
          Brick.txt "Press "
          <+> Common.keyToWidget (menu ^. menuKeyBindingsL % mKbQuitL)
          <+> Brick.txt " to go back, Press Enter to edit the highlighted field"
      ]
    fieldLabels  = [field & fieldLabel | field <- menu ^. menuFieldsL]
    buttonLabels = [button & fieldLabel | button <- menu ^. menuButtonsL]
    allLabels    = fieldLabels ++ buttonLabels

    maxWidth = foldl' max 5 (fmap Brick.textWidth allLabels)

    -- A list of functions which draw a highlighted label with right padding at the left of a widget.
    amplifiers =
      let labelsWidgets = fmap renderAslabel fieldLabels
       in fmap (\f b -> (rightify (maxWidth + 1) (f b <+> Brick.txt " ") <+>) ) labelsWidgets
    drawFields = fmap drawField amplifiers
    fieldWidgets = zipWith (F.withFocusRing (menu ^. menuFocusRingL)) drawFields (menu ^. menuFieldsL)

    buttonAmplifiers =
      let buttonAsWidgets = fmap renderAslabel buttonLabels
       in fmap (\f b -> ((leftify (maxWidth + 2) . Border.border $ f b) <+>) ) buttonAsWidgets
    drawButtons = fmap drawField buttonAmplifiers
    buttonWidgets = zipWith (F.withFocusRing (menu ^. menuFocusRingL)) drawButtons (menu ^. menuButtonsL)

    overlays = catMaybes $ fmap drawFieldOverlay (menu ^. menuFieldsL)