packages feed

brick-1.5: src/Brick/Widgets/Dialog.hs

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE CPP #-}
-- | This module provides a simple dialog widget. You get to pick the
-- dialog title, if any, as well as its body and buttons.
--
-- Note that this dialog is really for simple use cases where you want
-- to get the user's answer to a question, such as "Would you like to
-- save changes before quitting?" As is typical in such cases, we assume
-- that this dialog box is used modally, meaning that while it is open
-- it is has exclusive input focus until it is closed.
--
-- If you require something more sophisticated, you'll need to build it
-- yourself. You might also consider seeing the 'Brick.Forms' module for
-- help with input management and see the implementation of this module
-- to see how to reproduce a dialog-style UI.
module Brick.Widgets.Dialog
  ( Dialog
  , dialogTitle
  , dialogButtons
  , dialogWidth
  -- * Construction and rendering
  , dialog
  , renderDialog
  , getDialogFocus
  , setDialogFocus
  -- * Handling events
  , handleDialogEvent
  -- * Getting a dialog's current value
  , dialogSelection
  -- * Attributes
  , dialogAttr
  , buttonAttr
  , buttonSelectedAttr
  -- * Lenses
  , dialogButtonsL
  , dialogWidthL
  , dialogTitleL
  )
where

import Lens.Micro
import Lens.Micro.Mtl ((%=))
#if !(MIN_VERSION_base(4,11,0))
import Data.Monoid
#endif
import Data.List (intersperse, find)
import Graphics.Vty.Input (Event(..), Key(..))

import Brick.Focus
import Brick.Types
import Brick.Widgets.Core
import Brick.Widgets.Center
import Brick.Widgets.Border
import Brick.AttrMap

-- | Dialogs present a window with a title (optional), a body, and
-- buttons (optional). Dialog buttons are labeled with strings and map
-- to values of type 'a', which you choose.
--
-- Dialogs handle the following events by default with
-- handleDialogEvent:
--
-- * Tab or Right Arrow: select the next button
-- * Shift-tab or Left Arrow: select the previous button
data Dialog a n =
    Dialog { dialogTitle :: Maybe (Widget n)
           -- ^ The dialog title
           , dialogButtons :: [(String, n, a)]
           -- ^ The dialog buttons' labels, resource names, and values
           , dialogWidth :: Int
           -- ^ The maximum width of the dialog
           , dialogFocus :: FocusRing n
           -- ^ The focus ring for the dialog's buttons
           }

suffixLenses ''Dialog

handleDialogEvent :: Event -> EventM n (Dialog a n) ()
handleDialogEvent ev = do
    case ev of
        EvKey (KChar '\t') [] -> dialogFocusL %= focusNext
        EvKey KRight []       -> dialogFocusL %= focusNext
        EvKey KBackTab []     -> dialogFocusL %= focusPrev
        EvKey KLeft []        -> dialogFocusL %= focusPrev
        _ -> return ()

-- | Set the focused button of a dialog.
setDialogFocus :: (Eq n) => n -> Dialog a n -> Dialog a n
setDialogFocus n d = d { dialogFocus = focusSetCurrent n $ dialogFocus d }

-- | Get the focused button of a dialog.
getDialogFocus :: Dialog a n -> Maybe n
getDialogFocus = focusGetCurrent . dialogFocus

-- | Create a dialog.
dialog :: (Eq n)
       => Maybe (Widget n)
       -- ^ The dialog title
       -> Maybe (n, [(String, n, a)])
       -- ^ The currently-selected button resource name and the button
       -- labels, resource names, and values to use for each button,
       -- respectively
       -> Int
       -- ^ The maximum width of the dialog
       -> Dialog a n
dialog title buttonData w =
    let (r, buttons) = case buttonData of
            Nothing ->
                (focusRing [], [])
            Just (focName, entries) ->
                let ns = (\(_, n, _) -> n) <$> entries
                in (focusSetCurrent focName $ focusRing ns, entries)
    in Dialog title buttons w r

-- | The default attribute of the dialog
dialogAttr :: AttrName
dialogAttr = attrName "dialog"

-- | The default attribute for all dialog buttons
buttonAttr :: AttrName
buttonAttr = attrName "button"

-- | The attribute for the selected dialog button (extends 'dialogAttr')
buttonSelectedAttr :: AttrName
buttonSelectedAttr = buttonAttr <> attrName "selected"

-- | Render a dialog with the specified body widget. This renders the
-- dialog as a layer, which makes this suitable as a top-level layer in
-- your rendering function to be rendered on top of the rest of your
-- interface.
renderDialog :: (Ord n) => Dialog a n -> Widget n -> Widget n
renderDialog d body =
    let buttonPadding = str "   "
        foc = focusGetCurrent $ dialogFocus d
        mkButton (s, n, _) =
            let att = if Just n == foc
                      then buttonSelectedAttr
                      else buttonAttr
                csr = if Just n == foc
                      then putCursor n (Location (1,0))
                      else id
            in csr $
               clickable n $
               withAttr att $
               str $ "  " <> s <> "  "
        buttons = hBox $ intersperse buttonPadding $
                         mkButton <$> (d^.dialogButtonsL)

        doBorder = maybe border borderWithLabel (d^.dialogTitleL)
    in centerLayer $
       withDefAttr dialogAttr $
       hLimit (d^.dialogWidthL) $
       doBorder $
       vBox [ body
            , hCenter buttons
            ]

-- | Obtain the resource name and value associated with the dialog's
-- currently-selected button, if any. The result of this function is
-- probably what you want when someone presses 'Enter' in a dialog.
dialogSelection :: (Eq n) => Dialog a n -> Maybe (n, a)
dialogSelection d = do
    n' <- focusGetCurrent $ dialogFocus d
    let matches (_, n, _) = n == n'
    (_, n, a) <- find matches (d^.dialogButtonsL)
    return (n, a)