{-| Module : Monomer.Widgets.Containers.Dropdown Copyright : (c) 2018 Francisco Vallarino License : BSD-3-Clause (see the LICENSE file) Maintainer : fjvallarino@gmail.com Stability : experimental Portability : non-portable Dropdown widget, allowing selection of a single item from a collapsable list. Both header and list content are customizable, and so is their styling. In case only 'Text' content is needed, "Monomer.Widgets.Singles.TextDropdown" is easier to use. @ makeSelected username = hstack [ label "Selected: ", spacer, label username ] makeRow username = label username customDropdown = dropdown userLens usernames makeSelected makeRow @ Note: the content of the dropdown list will only be updated when the provided items change, based on their 'Eq' instance. In case data external to the items is used for building the row nodes, 'mergeRequired' may be needed to avoid stale content. -} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StrictData #-} module Monomer.Widgets.Containers.Dropdown ( -- * Configuration DropdownCfg, DropdownItem, -- * Constructors dropdown, dropdown_, dropdownV, dropdownV_, dropdownD_ ) where import Control.Applicative ((<|>)) import Control.Lens (ALens', (&), (^.), (^?), (^?!), (.~), (%~), (<>~), _Just, ix, non) import Control.Monad import Data.Default import Data.List (foldl') import Data.Maybe import Data.Sequence (Seq(..), (<|), (|>)) import Data.Text (Text) import Data.Typeable (Typeable, Proxy, cast, typeRep) import GHC.Generics import TextShow import qualified Data.Sequence as Seq import Monomer.Helper import Monomer.Widgets.Container import Monomer.Widgets.Containers.SelectList import Monomer.Widgets.Singles.Label import qualified Monomer.Lens as L -- | Constraints for an item handled by dropdown. type DropdownItem a = SelectListItem a {-| Configuration options for dropdown: - 'onFocus': event to raise when focus is received. - 'onFocusReq': 'WidgetRequest' to generate when focus is received. - 'onBlur': event to raise when focus is lost. - 'onBlurReq': 'WidgetRequest' to generate when focus is lost. - 'onChange': event to raise when selected item changes. - 'onChangeReq': 'WidgetRequest' to generate when selected item changes. - 'onChangeIdx': event to raise when selected item changes. Includes index, - 'onChangeIdxReq': 'WidgetRequest' to generate when selected item changes. Includes index. - 'maxHeight': maximum height of the list when dropdown is expanded. - 'itemBasicStyle': 'Style' of an item in the list when not selected. - 'itemSelectedStyle': 'Style' of the selected item in the list. - 'mergeRequired': whether merging the items in the list is required. Useful when the content displayed depends on external data, since changes to data outside the provided list cannot be detected. In general it is recommended to only depend on data contained in the list itself, making sure the 'Eq' instance of the item type is correct. -} data DropdownCfg s e a = DropdownCfg { _ddcMaxHeight :: Maybe Double, _ddcItemStyle :: Maybe Style, _ddcItemSelectedStyle :: Maybe Style, _ddcMergeRequired :: Maybe (WidgetEnv s e -> Seq a -> Seq a -> Bool), _ddcOnFocusReq :: [Path -> WidgetRequest s e], _ddcOnBlurReq :: [Path -> WidgetRequest s e], _ddcOnChangeReq :: [a -> WidgetRequest s e], _ddcOnChangeIdxReq :: [Int -> a -> WidgetRequest s e] } instance Default (DropdownCfg s e a) where def = DropdownCfg { _ddcMaxHeight = Nothing, _ddcItemStyle = Nothing, _ddcItemSelectedStyle = Nothing, _ddcMergeRequired = Nothing, _ddcOnFocusReq = [], _ddcOnBlurReq = [], _ddcOnChangeReq = [], _ddcOnChangeIdxReq = [] } instance Semigroup (DropdownCfg s e a) where (<>) t1 t2 = DropdownCfg { _ddcMaxHeight = _ddcMaxHeight t2 <|> _ddcMaxHeight t1, _ddcItemStyle = _ddcItemStyle t2 <|> _ddcItemStyle t1, _ddcItemSelectedStyle = _ddcItemSelectedStyle t2 <|> _ddcItemSelectedStyle t1, _ddcMergeRequired = _ddcMergeRequired t2 <|> _ddcMergeRequired t1, _ddcOnFocusReq = _ddcOnFocusReq t1 <> _ddcOnFocusReq t2, _ddcOnBlurReq = _ddcOnBlurReq t1 <> _ddcOnBlurReq t2, _ddcOnChangeReq = _ddcOnChangeReq t1 <> _ddcOnChangeReq t2, _ddcOnChangeIdxReq = _ddcOnChangeIdxReq t1 <> _ddcOnChangeIdxReq t2 } instance Monoid (DropdownCfg s e a) where mempty = def instance WidgetEvent e => CmbOnFocus (DropdownCfg s e a) e Path where onFocus fn = def { _ddcOnFocusReq = [RaiseEvent . fn] } instance CmbOnFocusReq (DropdownCfg s e a) s e Path where onFocusReq req = def { _ddcOnFocusReq = [req] } instance WidgetEvent e => CmbOnBlur (DropdownCfg s e a) e Path where onBlur fn = def { _ddcOnBlurReq = [RaiseEvent . fn] } instance CmbOnBlurReq (DropdownCfg s e a) s e Path where onBlurReq req = def { _ddcOnBlurReq = [req] } instance WidgetEvent e => CmbOnChange (DropdownCfg s e a) a e where onChange fn = def { _ddcOnChangeReq = [RaiseEvent . fn] } instance CmbOnChangeReq (DropdownCfg s e a) s e a where onChangeReq req = def { _ddcOnChangeReq = [req] } instance WidgetEvent e => CmbOnChangeIdx (DropdownCfg s e a) e a where onChangeIdx fn = def { _ddcOnChangeIdxReq = [(RaiseEvent .) . fn] } instance CmbOnChangeIdxReq (DropdownCfg s e a) s e a where onChangeIdxReq req = def { _ddcOnChangeIdxReq = [req] } instance CmbMaxHeight (DropdownCfg s e a) where maxHeight h = def { _ddcMaxHeight = Just h } instance CmbItemBasicStyle (DropdownCfg s e a) Style where itemBasicStyle style = def { _ddcItemStyle = Just style } instance CmbItemSelectedStyle (DropdownCfg s e a) Style where itemSelectedStyle style = def { _ddcItemSelectedStyle = Just style } instance CmbMergeRequired (DropdownCfg s e a) (WidgetEnv s e) (Seq a) where mergeRequired fn = def { _ddcMergeRequired = Just fn } data DropdownState = DropdownState { _ddsOpen :: Bool, _ddsOffset :: Point } deriving (Eq, Show, Generic) data DropdownMessage = forall a . DropdownItem a => OnChangeMessage Int a | OnListBlur -- | Creates a dropdown using the given lens. dropdown :: (WidgetModel s, WidgetEvent e, Traversable t, DropdownItem a) => ALens' s a -- ^ The lens into the model. -> t a -- ^ The list of selectable items. -> (a -> WidgetNode s e) -- ^ Function to create the header (always visible). -> (a -> WidgetNode s e) -- ^ Function to create the list (collapsable). -> WidgetNode s e -- ^ The created dropdown. dropdown field items makeMain makeRow = newNode where newNode = dropdown_ field items makeMain makeRow def -- | Creates a dropdown using the given lens. Accepts config. dropdown_ :: (WidgetModel s, WidgetEvent e, Traversable t, DropdownItem a) => ALens' s a -- ^ The lens into the model. -> t a -- ^ The list of selectable items. -> (a -> WidgetNode s e) -- ^ Function to create the header (always visible). -> (a -> WidgetNode s e) -- ^ Function to create the list (collapsable). -> [DropdownCfg s e a] -- ^ The config options. -> WidgetNode s e -- ^ The created dropdown. dropdown_ field items makeMain makeRow configs = newNode where widgetData = WidgetLens field newNode = dropdownD_ widgetData items makeMain makeRow configs -- | Creates a dropdown using the given value and 'onChange' event handler. dropdownV :: (WidgetModel s, WidgetEvent e, Traversable t, DropdownItem a) => a -- ^ The current value. -> (Int -> a -> e) -- ^ The event to raise on change. -> t a -- ^ The list of selectable items. -> (a -> WidgetNode s e) -- ^ Function to create the header (always visible). -> (a -> WidgetNode s e) -- ^ Function to create the list (collapsable). -> WidgetNode s e -- ^ The created dropdown. dropdownV value handler items makeMain makeRow = newNode where newNode = dropdownV_ value handler items makeMain makeRow def -- | Creates a dropdown using the given value and 'onChange' event handler. -- | Accepts config. dropdownV_ :: (WidgetModel s, WidgetEvent e, Traversable t, DropdownItem a) => a -- ^ The current value. -> (Int -> a -> e) -- ^ The event to raise on change. -> t a -- ^ The list of selectable items. -> (a -> WidgetNode s e) -- ^ Function to create the header (always visible). -> (a -> WidgetNode s e) -- ^ Function to create the list (collapsable). -> [DropdownCfg s e a] -- ^ The config options. -> WidgetNode s e -- ^ The created dropdown. dropdownV_ value handler items makeMain makeRow configs = newNode where newConfigs = onChangeIdx handler : configs newNode = dropdownD_ (WidgetValue value) items makeMain makeRow newConfigs -- | Creates a dropdown providing a WidgetData instance and config. dropdownD_ :: forall s e t a . (WidgetModel s, WidgetEvent e, Traversable t, DropdownItem a) => WidgetData s a -- ^ The 'WidgetData' to retrieve the value from. -> t a -- ^ The list of selectable items. -> (a -> WidgetNode s e) -- ^ Function to create the header (always visible). -> (a -> WidgetNode s e) -- ^ Function to create the list (collapsable). -> [DropdownCfg s e a] -- ^ The config options. -> WidgetNode s e -- ^ The created dropdown. dropdownD_ widgetData items makeMain makeRow configs = newNode where config = mconcat configs newState = DropdownState False def newItems = foldl' (|>) Empty items wtype = WidgetType ("dropdown-" <> showt (typeRep (undefined :: Proxy a))) widget = makeDropdown widgetData newItems makeMain makeRow config newState newNode = defaultWidgetNode wtype widget & L.info . L.focusable .~ True makeDropdown :: forall s e a. (WidgetModel s, WidgetEvent e, DropdownItem a) => WidgetData s a -> Seq a -> (a -> WidgetNode s e) -> (a -> WidgetNode s e) -> DropdownCfg s e a -> DropdownState -> Widget s e makeDropdown widgetData items makeMain makeRow config state = widget where container = def { containerAddStyleReq = False, containerChildrenOffset = Just (_ddsOffset state), containerGetBaseStyle = getBaseStyle, containerInit = init, containerFindNextFocus = findNextFocus, containerFindByPoint = findByPoint, containerMerge = merge, containerDispose = dispose, containerHandleEvent = handleEvent, containerHandleMessage = handleMessage, containerGetSizeReq = getSizeReq, containerResize = resize } baseWidget = createContainer state container widget = baseWidget { widgetRender = render } mainIdx = 0 listIdx = 1 isOpen = _ddsOpen state currentValue wenv = widgetDataGet (_weModel wenv) widgetData createDropdown wenv node newState = newNode where selected = currentValue wenv nodeStyle = _wnInfo node ^. L.style mainNode = makeMain selected & L.info . L.style .~ nodeStyle widgetId = node ^. L.info . L.widgetId selectListNode = makeSelectList wenv widgetData items makeRow config widgetId newWidget = makeDropdown widgetData items makeMain makeRow config newState newNode = node & L.widget .~ newWidget & L.children .~ Seq.fromList [mainNode, selectListNode] getBaseStyle wenv node = Just style where style = collectTheme wenv L.dropdownStyle init wenv node = resultNode $ createDropdown wenv node state merge wenv newNode oldNode oldState = result where result = resultNode $ createDropdown wenv newNode oldState dispose wenv node = resultReqs node reqs where widgetId = node ^. L.info . L.widgetId reqs = [ ResetOverlay widgetId | isOpen ] findNextFocus wenv node direction start | isOpen = node ^. L.children | otherwise = Empty findByPoint wenv node start point = result where children = node ^. L.children mainNode = Seq.index children mainIdx listNode = Seq.index children listIdx result | isOpen && isPointInNodeVp listNode point = Just listIdx | not isOpen && isPointInNodeVp mainNode point = Just mainIdx | otherwise = Nothing ddFocusChange node prev reqs = Just newResult where tmpResult = handleFocusChange node prev reqs newResult = fromMaybe (resultNode node) tmpResult & L.requests %~ (|> IgnoreChildrenEvents) handleEvent wenv node target evt = case evt of Focus prev | not isOpen -> ddFocusChange node prev (_ddcOnFocusReq config) Blur next | not isOpen && not (seqStartsWith path focusedPath) -> ddFocusChange node next (_ddcOnBlurReq config) Move point -> result where mainNode = Seq.index (node ^. L.children) mainIdx listNode = Seq.index (node ^. L.children) listIdx slPoint = addPoint (negPoint (_ddsOffset state)) point validMainPos = not isOpen && isPointInNodeVp mainNode point validListPos = isOpen && isPointInNodeVp listNode slPoint validPos = validMainPos || validListPos isArrow = Just CursorArrow == (snd <$> wenv ^. L.cursor) resetRes = resultReqs node [SetCursorIcon widgetId CursorArrow] result | not validPos && not isArrow = Just resetRes | otherwise = Nothing ButtonAction _ btn BtnPressed _ | btn == wenv ^. L.mainButton && not isOpen -> result where result = Just $ resultReqs node [SetFocus (node ^. L.info . L.widgetId)] Click point _ _ | openRequired point node -> Just resultOpen | closeRequired point node -> Just resultClose where inVp = isPointInNodeVp node point resultOpen = openDropdown wenv node & L.requests <>~ Seq.fromList [SetCursorIcon widgetId CursorArrow] resultClose = closeDropdown wenv node & L.requests <>~ Seq.fromList [ResetCursorIcon widgetId | not inVp] KeyAction mode code KeyPressed | isKeyOpenDropdown && not isOpen -> Just $ openDropdown wenv node | isKeyEscape code && isOpen -> Just $ closeDropdown wenv node where activationKeys = [isKeyDown, isKeyUp, isKeySpace, isKeyReturn] isKeyOpenDropdown = or (fmap ($ code) activationKeys) _ | not isOpen -> Just $ resultReqs node [IgnoreChildrenEvents] | otherwise -> Nothing where style = currentStyle wenv node widgetId = node ^. L.info . L.widgetId path = node ^. L.info . L.path focusedPath = wenv ^. L.focusedPath overlayPath = wenv ^. L.overlayPath overlayParent = isNodeParentOfPath node (fromJust overlayPath) nodeValid = isNothing overlayPath || overlayParent openRequired point node = not isOpen && inViewport where inViewport = pointInRect point (node ^. L.info . L.viewport) closeRequired point node = isOpen && not inOverlay where offset = _ddsOffset state listNode = Seq.index (node ^. L.children) listIdx listVp = moveRect offset (listNode ^. L.info . L.viewport) inOverlay = pointInRect point listVp openDropdown wenv node = resultReqs newNode requests where newState = state { _ddsOpen = True, _ddsOffset = listOffset wenv node } newNode = node & L.widget .~ makeDropdown widgetData items makeMain makeRow config newState -- selectList is wrapped by a scroll widget (slWid, slPath) = scrollListInfo node (listWid, _) = selectListInfo node scrollMsg = SendMessage listWid SelectListShowSelected requests = [SetOverlay slWid slPath, SetFocus listWid, scrollMsg] closeDropdown wenv node = resultReqs newNode requests where widgetId = node ^. L.info . L.widgetId (slWid, _) = scrollListInfo node (listWid, _) = selectListInfo node newState = state { _ddsOpen = False, _ddsOffset = def } newNode = node & L.widget .~ makeDropdown widgetData items makeMain makeRow config newState requests = [ResetOverlay slWid, SetFocus widgetId] scrollListInfo :: WidgetNode s e -> (WidgetId, Path) scrollListInfo node = (scrollInfo ^. L.widgetId, scrollInfo ^. L.path) where scrollInfo = node ^?! L.children . ix listIdx . L.info selectListInfo :: WidgetNode s e -> (WidgetId, Path) selectListInfo node = (listInfo ^. L.widgetId, listInfo ^. L.path) where listInfo = node ^?! L.children . ix listIdx . L.children . ix 0 . L.info handleMessage wenv node target msg = cast msg >>= handleLvMsg wenv node handleLvMsg wenv node (OnChangeMessage idx _) = Seq.lookup idx items >>= \value -> Just $ onChange wenv node idx value handleLvMsg wenv node OnListBlur = Just result where tempResult = closeDropdown wenv node result = tempResult & L.requests %~ (|> createMoveFocusReq wenv) onChange wenv node idx item = result where WidgetResult newNode reqs = closeDropdown wenv node newReqs = Seq.fromList $ widgetDataSet widgetData item ++ fmap ($ item) (_ddcOnChangeReq config) ++ fmap (\fn -> fn idx item) (_ddcOnChangeIdxReq config) result = WidgetResult newNode (reqs <> newReqs) getSizeReq :: ContainerGetSizeReqHandler s e getSizeReq wenv node children = (newReqW, newReqH) where -- Main section reqs mainC = Seq.index children 0 mainReqW = mainC ^. L.info . L.sizeReqW mainReqH = mainC ^. L.info . L.sizeReqH -- List items reqs listC = Seq.index children 1 listReqW = listC ^. L.info . L.sizeReqW -- Items other than main could be wider -- Height only matters for the selected item, since the rest is in a scroll newReqW = sizeReqMergeMax mainReqW listReqW newReqH = mainReqH listHeight wenv node = maxHeight where Size _ winH = _weWindowSize wenv theme = currentTheme wenv node maxHeightTheme = theme ^. L.dropdownMaxHeight cfgMaxHeight = _ddcMaxHeight config -- Avoid having an invisible list if style/theme is not set maxHeightStyle = max 20 $ fromMaybe maxHeightTheme cfgMaxHeight reqHeight = case Seq.lookup 1 (node ^. L.children) of Just child -> sizeReqMaxBounded $ child ^. L.info . L.sizeReqH _ -> 0 maxHeight = min winH (min reqHeight maxHeightStyle) listOffset wenv node = Point 0 newOffset where Size _ winH = _weWindowSize wenv viewport = node ^. L.info . L.viewport scOffset = wenv ^. L.offset Rect rx ry rw rh = moveRect scOffset viewport lh = listHeight wenv node newOffset | ry + rh + lh > winH = - (rh + lh) | otherwise = 0 resize wenv node viewport children = resized where style = currentStyle wenv node Rect rx ry rw rh = viewport !mainArea = viewport !listArea = viewport { _rY = ry + rh, _rH = listHeight wenv node } assignedAreas = Seq.fromList [mainArea, listArea] resized = (resultNode node, assignedAreas) render wenv node renderer = do drawInScissor renderer True viewport $ drawStyledAction renderer viewport style $ \contentArea -> do widgetRender (mainNode ^. L.widget) wenv mainNode renderer renderArrow renderer style contentArea when isOpen $ createOverlay renderer $ drawInTranslation renderer totalOffset $ do renderOverlay renderer cwenv listOverlay where style = currentStyle wenv node viewport = node ^. L.info . L.viewport mainNode = Seq.index (node ^. L.children) mainIdx -- List view is rendered with an offset to accommodate for window height listOverlay = Seq.index (node ^. L.children) listIdx listOverlayVp = listOverlay ^. L.info . L.viewport scOffset = wenv ^. L.offset offset = _ddsOffset state totalOffset = addPoint scOffset offset cwenv = updateWenvOffset container wenv node listOverlayVp & L.viewport .~ listOverlayVp renderArrow renderer style contentArea = drawArrowDown renderer arrowRect (_sstFgColor style) where Rect x y w h = contentArea size = style ^. L.text . non def . L.fontSize . non def arrowW = unFontSize size / 2 arrowRect = Rect (x + w - arrowW) (y + h / 2 - arrowW / 3) arrowW (arrowW / 2) renderOverlay renderer wenv overlayNode = renderAction where widget = overlayNode ^. L.widget renderAction = widgetRender widget wenv overlayNode renderer makeSelectList :: (WidgetModel s, WidgetEvent e, DropdownItem a) => WidgetEnv s e -> WidgetData s a -> Seq a -> (a -> WidgetNode s e) -> DropdownCfg s e a -> WidgetId -> WidgetNode s e makeSelectList wenv value items makeRow config widgetId = selectListNode where normalTheme = collectTheme wenv L.dropdownItemStyle selectedTheme = collectTheme wenv L.dropdownItemSelectedStyle itemStyle = fromJust (Just normalTheme <> _ddcItemStyle config) itemSelStyle = fromJust (Just selectedTheme <> _ddcItemSelectedStyle config) mergeReqFn = maybe def mergeRequired (_ddcMergeRequired config) slConfig = [ selectOnBlur, onBlurReq (const $ SendMessage widgetId OnListBlur), onChangeIdxReq (\idx it -> SendMessage widgetId (OnChangeMessage idx it)), itemBasicStyle itemStyle, itemSelectedStyle itemSelStyle, mergeReqFn ] slStyle = collectTheme wenv L.dropdownListStyle selectListNode = selectListD_ value items makeRow slConfig & L.info . L.style .~ slStyle createMoveFocusReq :: WidgetEnv s e -> WidgetRequest s e createMoveFocusReq wenv = MoveFocus Nothing direction where direction | wenv ^. L.inputStatus . L.keyMod . L.leftShift = FocusBwd | otherwise = FocusFwd