{-# 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)