monomer-1.3.0.0: src/Monomer/Widgets/Containers/SelectList.hs
{-|
Module : Monomer.Widgets.Containers.SelectList
Copyright : (c) 2018 Francisco Vallarino
License : BSD-3-Clause (see the LICENSE file)
Maintainer : fjvallarino@gmail.com
Stability : experimental
Portability : non-portable
Select list widget, allowing selection of a single item. List content (rows) is
customizable, plus its styling.
-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Monomer.Widgets.Containers.SelectList (
-- * Configuration
SelectListCfg,
SelectListItem,
SelectListMessage(..),
SelectListMakeRow,
-- * Constructors
selectList,
selectList_,
selectListV,
selectListV_,
selectListD_
) where
import Control.Applicative ((<|>))
import Control.Lens (ALens', (&), (^.), (^?), (^?!), (.~), (%~), (?~), (<>~), at, ix, non, _Just)
import Control.Monad (when)
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 TextShow
import qualified Data.Map as Map
import qualified Data.Sequence as Seq
import Monomer.Graphics.Lens
import Monomer.Widgets.Container
import Monomer.Widgets.Containers.Box
import Monomer.Widgets.Containers.Scroll
import Monomer.Widgets.Containers.Stack
import Monomer.Widgets.Singles.Label
import Monomer.Widgets.Singles.Spacer
import qualified Monomer.Lens as L
-- | Constraints for an item handled by selectList.
type SelectListItem a = (Eq a, Show a, Typeable a)
-- | Creates a row from an item.
type SelectListMakeRow s e a = a -> WidgetNode s e
{-|
Configuration options for selectList:
- '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.
- 'selectOnBlur': whether to select the currently highlighted item when
navigating away from the widget with tab key.
- 'itemBasicStyle': style of an item in the list when not selected.
- 'itemSelectedStyle': style of the selected item in the list.
- 'mergeRequired': whether merging children is required. Useful when select list
is part of another widget such as dropdown.
-}
data SelectListCfg s e a = SelectListCfg {
_slcSelectOnBlur :: Maybe Bool,
_slcItemStyle :: Maybe Style,
_slcItemSelectedStyle :: Maybe Style,
_slcMergeRequired :: Maybe (Seq a -> Seq a -> Bool),
_slcOnFocusReq :: [Path -> WidgetRequest s e],
_slcOnBlurReq :: [Path -> WidgetRequest s e],
_slcOnChangeReq :: [a -> WidgetRequest s e],
_slcOnChangeIdxReq :: [Int -> a -> WidgetRequest s e]
}
instance Default (SelectListCfg s e a) where
def = SelectListCfg {
_slcSelectOnBlur = Nothing,
_slcItemStyle = Nothing,
_slcItemSelectedStyle = Nothing,
_slcMergeRequired = Nothing,
_slcOnFocusReq = [],
_slcOnBlurReq = [],
_slcOnChangeReq = [],
_slcOnChangeIdxReq = []
}
instance Semigroup (SelectListCfg s e a) where
(<>) t1 t2 = SelectListCfg {
_slcSelectOnBlur = _slcSelectOnBlur t2 <|> _slcSelectOnBlur t1,
_slcItemStyle = _slcItemStyle t2 <|> _slcItemStyle t1,
_slcItemSelectedStyle = _slcItemSelectedStyle t2 <|> _slcItemSelectedStyle t1,
_slcMergeRequired = _slcMergeRequired t2 <|> _slcMergeRequired t1,
_slcOnFocusReq = _slcOnFocusReq t1 <> _slcOnFocusReq t2,
_slcOnBlurReq = _slcOnBlurReq t1 <> _slcOnBlurReq t2,
_slcOnChangeReq = _slcOnChangeReq t1 <> _slcOnChangeReq t2,
_slcOnChangeIdxReq = _slcOnChangeIdxReq t1 <> _slcOnChangeIdxReq t2
}
instance Monoid (SelectListCfg s e a) where
mempty = def
instance WidgetEvent e => CmbOnFocus (SelectListCfg s e a) e Path where
onFocus fn = def {
_slcOnFocusReq = [RaiseEvent . fn]
}
instance CmbOnFocusReq (SelectListCfg s e a) s e Path where
onFocusReq req = def {
_slcOnFocusReq = [req]
}
instance WidgetEvent e => CmbOnBlur (SelectListCfg s e a) e Path where
onBlur fn = def {
_slcOnBlurReq = [RaiseEvent . fn]
}
instance CmbOnBlurReq (SelectListCfg s e a) s e Path where
onBlurReq req = def {
_slcOnBlurReq = [req]
}
instance WidgetEvent e => CmbOnChange (SelectListCfg s e a) a e where
onChange fn = def {
_slcOnChangeReq = [RaiseEvent . fn]
}
instance CmbOnChangeReq (SelectListCfg s e a) s e a where
onChangeReq req = def {
_slcOnChangeReq = [req]
}
instance WidgetEvent e => CmbOnChangeIdx (SelectListCfg s e a) e a where
onChangeIdx fn = def {
_slcOnChangeIdxReq = [(RaiseEvent .) . fn]
}
instance CmbOnChangeIdxReq (SelectListCfg s e a) s e a where
onChangeIdxReq req = def {
_slcOnChangeIdxReq = [req]
}
instance CmbSelectOnBlur (SelectListCfg s e a) where
selectOnBlur_ select = def {
_slcSelectOnBlur = Just select
}
instance CmbItemBasicStyle (SelectListCfg s e a) Style where
itemBasicStyle style = def {
_slcItemStyle = Just style
}
instance CmbItemSelectedStyle (SelectListCfg s e a) Style where
itemSelectedStyle style = def {
_slcItemSelectedStyle = Just style
}
instance CmbMergeRequired (SelectListCfg s e a) (Seq a) where
mergeRequired fn = def {
_slcMergeRequired = Just fn
}
data SelectListState a = SelectListState {
_prevItems :: Seq a,
_slIdx :: Int,
_hlIdx :: Int
} deriving (Eq, Show)
-- | Messages received by selectList. In general used internally.
data SelectListMessage
= SelectListClickItem Int
| SelectListShowSelected
deriving (Eq, Show)
-- | Creates a select list using the given lens.
selectList
:: (WidgetModel s, WidgetEvent e, Traversable t, SelectListItem a)
=> ALens' s a -- ^ The lens into the model.
-> t a -- ^ The list of selectable items.
-> SelectListMakeRow s e a -- ^ Function to create the list items.
-> WidgetNode s e -- ^ The created dropdown.
selectList field items makeRow = selectList_ field items makeRow def
-- | Creates a select list using the given lens. Accepts config.
selectList_
:: (WidgetModel s, WidgetEvent e, Traversable t, SelectListItem a)
=> ALens' s a -- ^ The lens into the model.
-> t a -- ^ The list of selectable items.
-> SelectListMakeRow s e a -- ^ Function to create the list items.
-> [SelectListCfg s e a] -- ^ The config options.
-> WidgetNode s e -- ^ The created dropdown.
selectList_ field items makeRow configs = newNode where
newNode = selectListD_ (WidgetLens field) items makeRow configs
-- | Creates a select list using the given value and 'onChange' event handler.
selectListV
:: (WidgetModel s, WidgetEvent e, Traversable t, SelectListItem a)
=> a -- ^ The event to raise on change.
-> (Int -> a -> e) -- ^ The list of selectable items.
-> t a -- ^ The list of selectable items.
-> SelectListMakeRow s e a -- ^ Function to create the list items.
-> WidgetNode s e -- ^ The created dropdown.
selectListV value handler items makeRow = newNode where
newNode = selectListV_ value handler items makeRow def
-- | Creates a select list using the given value and 'onChange' event handler.
-- Accepts config.
selectListV_
:: (WidgetModel s, WidgetEvent e, Traversable t, SelectListItem a)
=> a -- ^ The event to raise on change.
-> (Int -> a -> e) -- ^ The list of selectable items.
-> t a -- ^ The list of selectable items.
-> SelectListMakeRow s e a -- ^ Function to create the list items.
-> [SelectListCfg s e a] -- ^ The config options.
-> WidgetNode s e -- ^ The created dropdown.
selectListV_ value handler items makeRow configs = newNode where
widgetData = WidgetValue value
newConfigs = onChangeIdx handler : configs
newNode = selectListD_ widgetData items makeRow newConfigs
-- | Creates a dropdown providing a 'WidgetData' instance and config.
selectListD_
:: forall s e t a . (WidgetModel s, WidgetEvent e, Traversable t, SelectListItem a)
=> WidgetData s a -- ^ The 'WidgetData' to retrieve the value from.
-> t a -- ^ The list of selectable items.
-> SelectListMakeRow s e a -- ^ Function to create the list items.
-> [SelectListCfg s e a] -- ^ The config options.
-> WidgetNode s e -- ^ The created dropdown.
selectListD_ widgetData items makeRow configs = makeNode wtype widget where
config = mconcat configs
newItems = foldl' (|>) Empty items
newState = SelectListState newItems (-1) 0
wtype = WidgetType ("selectList-" <> showt (typeRep (undefined :: Proxy a)))
widget = makeSelectList widgetData newItems makeRow config newState
makeNode :: WidgetType -> Widget s e -> WidgetNode s e
makeNode wtype widget = scroll_ [scrollStyle L.selectListStyle] childNode where
childNode = defaultWidgetNode wtype widget
& L.info . L.focusable .~ True
makeSelectList
:: (WidgetModel s, WidgetEvent e, SelectListItem a)
=> WidgetData s a
-> Seq a
-> SelectListMakeRow s e a
-> SelectListCfg s e a
-> SelectListState a
-> Widget s e
makeSelectList widgetData items makeRow config state = widget where
widget = createContainer state def {
containerInit = init,
containerInitPost = initPost,
containerMergeChildrenReq = mergeChildrenReq,
containerMerge = merge,
containerMergePost = mergePost,
containerHandleEvent = handleEvent,
containerHandleMessage = handleMessage
}
currentValue wenv = widgetDataGet (_weModel wenv) widgetData
createSelectListChildren wenv node = children where
widgetId = node ^. L.info . L.widgetId
selected = currentValue wenv
itemsList = makeItemsList wenv items makeRow config widgetId selected
children = Seq.singleton itemsList
init wenv node = resultNode newNode where
selected = currentValue wenv
newSl = fromMaybe (-1) (Seq.elemIndexL selected items)
newHl = if newSl < 0 then 0 else newSl
newState = state {
_slIdx = newSl,
_hlIdx = newHl
}
newNode = node
& L.widget .~ makeSelectList widgetData items makeRow config newState
& L.children .~ createSelectListChildren wenv node
initPost wenv node newState result = newResult where
newResult = updateResultStyle wenv config result state newState
mergeChildrenReq wenv node oldNode oldState = result where
oldItems = _prevItems oldState
mergeRequiredFn = fromMaybe (/=) (_slcMergeRequired config)
result = mergeRequiredFn oldItems items
merge wenv node oldNode oldState = resultNode newNode where
selected = currentValue wenv
newSl = fromMaybe (-1) (Seq.elemIndexL selected items)
newHl
| newSl /= _slIdx oldState = newSl
| otherwise = _hlIdx oldState
newState = oldState {
_slIdx = newSl,
_hlIdx = newHl,
_prevItems = items
}
newNode = node
& L.widget .~ makeSelectList widgetData items makeRow config newState
& L.children .~ createSelectListChildren wenv node
mergePost wenv node oldNode oldState newState result = newResult where
newResult = updateResultStyle wenv config result oldState newState
handleEvent wenv node target evt = case evt of
ButtonAction _ btn BtnPressed _
| btn == wenv ^. L.mainButton -> result where
result = Just $ resultReqs node [SetFocus (node ^. L.info . L.widgetId)]
Focus prev -> handleFocusChange node prev (_slcOnFocusReq config)
Blur next -> result where
tabPressed = wenv ^. L.inputStatus . L.keys . at keyTab == Just KeyPressed
changeReq = tabPressed && _slcSelectOnBlur config == Just True
WidgetResult tempNode tempReqs
| changeReq = selectItem wenv node (_hlIdx state)
| otherwise = resultNode node
reqs = tempReqs <> Seq.fromList (($ next) <$> _slcOnBlurReq config)
result
| changeReq || not (null reqs) = Just $ WidgetResult tempNode reqs
| otherwise = Nothing
KeyAction mode code status
| isKeyDown code && status == KeyPressed -> highlightNext wenv node
| isKeyUp code && status == KeyPressed -> highlightPrev wenv node
| isSelectKey code && status == KeyPressed -> resultSelected
where
resultSelected = Just $ selectItem wenv node (_hlIdx state)
isSelectKey code = isKeyReturn code || isKeySpace code
_ -> Nothing
highlightNext wenv node = highlightItem wenv node nextIdx where
tempIdx = _hlIdx state
nextIdx
| tempIdx < length items - 1 = tempIdx + 1
| otherwise = tempIdx
highlightPrev wenv node = highlightItem wenv node nextIdx where
tempIdx = _hlIdx state
nextIdx
| tempIdx > 0 = tempIdx - 1
| otherwise = tempIdx
handleMessage wenv node target message = result where
handleSelect (SelectListClickItem idx) = handleItemClick wenv node idx
handleSelect SelectListShowSelected = handleItemShow wenv node
result = fmap handleSelect (cast message)
handleItemClick wenv node idx = result where
focusReq = SetFocus $ node ^. L.info . L.widgetId
tempResult = selectItem wenv node idx
result
| isNodeFocused wenv node = tempResult
| otherwise = tempResult & L.requests %~ (|> focusReq)
handleItemShow wenv node = resultReqs node reqs where
reqs = itemScrollTo wenv node (_slIdx state)
highlightItem wenv node nextIdx = Just result where
newState = state {
_hlIdx = nextIdx
}
tmpNode = node
& L.widget .~ makeSelectList widgetData items makeRow config newState
slIdx = _slIdx state
(newNode, resizeReq) = updateStyles wenv config state tmpNode slIdx nextIdx
reqs = itemScrollTo wenv newNode nextIdx ++ resizeReq
result = resultReqs newNode reqs
selectItem wenv node idx = result where
selected = currentValue wenv
value = fromMaybe selected (Seq.lookup idx items)
valueSetReq = widgetDataSet widgetData value
scrollToReq = itemScrollTo wenv node idx
changeReqs = fmap ($ value) (_slcOnChangeReq config)
++ fmap (\fn -> fn idx value) (_slcOnChangeIdxReq config)
(styledNode, resizeReq) = updateStyles wenv config state node idx idx
newState = state {
_slIdx = idx,
_hlIdx = idx
}
newNode = styledNode
& L.widget .~ makeSelectList widgetData items makeRow config newState
reqs = valueSetReq ++ scrollToReq ++ changeReqs ++ resizeReq
result = resultReqs newNode reqs
itemScrollTo wenv node idx = maybeToList (scrollToReq <$> mwid <*> vp) where
vp = itemViewport node idx
mwid = findWidgetIdFromPath wenv (parentPath node)
scrollToReq wid rect = SendMessage wid (ScrollTo rect)
itemViewport node idx = viewport where
lookup idx node = Seq.lookup idx (node ^. L.children)
viewport = fmap (_wniViewport . _wnInfo) $ pure node
>>= lookup 0 -- vstack
>>= lookup idx -- item
updateStyles
:: WidgetEnv s e
-> SelectListCfg s e a
-> SelectListState a
-> WidgetNode s e
-> Int
-> Int
-> (WidgetNode s e, [WidgetRequest s e])
updateStyles wenv config state node newSlIdx newHlIdx = (newNode, newReqs) where
widgetId = node ^. L.info . L.widgetId
items = node ^. L.children . ix 0 . L.children
normalStyle = getNormalStyle wenv config
idxMatch = newSlIdx == newHlIdx
(slStyle, hlStyle)
| idxMatch = (getSlHlStyle wenv config, getSlHlStyle wenv config)
| otherwise = (getSlStyle wenv config, getHlStyle wenv config)
(newChildren, resizeReq) = (items, False)
& updateItemStyle wenv (_slIdx state) (Just normalStyle)
& updateItemStyle wenv (_hlIdx state) (Just normalStyle)
& updateItemStyle wenv newHlIdx (Just hlStyle)
& updateItemStyle wenv newSlIdx (Just slStyle)
newNode = node
& L.children . ix 0 . L.children .~ newChildren
newReqs = [ ResizeWidgets widgetId | resizeReq ]
updateItemStyle
:: WidgetEnv s e
-> Int
-> Maybe Style
-> (Seq (WidgetNode s e), Bool)
-> (Seq (WidgetNode s e), Bool)
updateItemStyle wenv idx mstyle (items, resizeReq) = result where
result = case Seq.lookup idx items of
Just item -> (newItems, resizeReq || newResizeReq) where
tmpItem = setItemStyle item mstyle
(newItem, newResizeReq) = updateItemSizeReq wenv tmpItem
newItems = Seq.update idx newItem items
Nothing -> (items, resizeReq)
updateItemSizeReq :: WidgetEnv s e -> WidgetNode s e -> (WidgetNode s e, Bool)
updateItemSizeReq wenv item = (newItem, resizeReq) where
(oldReqW, oldReqH) = (item^. L.info . L.sizeReqW, item^. L.info . L.sizeReqH)
(newReqW, newReqH) = widgetGetSizeReq (item ^. L.widget) wenv item
newItem = item
& L.info . L.sizeReqW .~ newReqW
& L.info . L.sizeReqH .~ newReqH
resizeReq = (oldReqW, oldReqH) /= (newReqW, newReqH)
setItemStyle :: WidgetNode s e -> Maybe Style -> WidgetNode s e
setItemStyle item Nothing = item
setItemStyle item (Just st) = item
& L.children . ix 0 . L.info . L.style .~ st
getSlStyle :: WidgetEnv s e -> SelectListCfg s e a -> Style
getSlStyle wenv config = style where
theme = collectTheme wenv L.selectListItemSelectedStyle
style = fromJust (Just theme <> _slcItemSelectedStyle config)
slStyle = style
& L.basic .~ style ^. L.focus
& L.hover .~ style ^. L.focusHover
getSlHlStyle :: WidgetEnv s e -> SelectListCfg s e a -> Style
getSlHlStyle wenv config = slStyle where
style = getSlStyle wenv config
slStyle = style
& L.basic .~ style ^. L.focus
& L.hover .~ style ^. L.focusHover
getHlStyle :: WidgetEnv s e -> SelectListCfg s e a -> Style
getHlStyle wenv config = hlStyle where
theme = collectTheme wenv L.selectListItemStyle
style = fromJust (Just theme <> _slcItemStyle config)
hlStyle = style
& L.basic .~ style ^. L.focus
& L.hover .~ style ^. L.focusHover
getNormalStyle :: WidgetEnv s e -> SelectListCfg s e a -> Style
getNormalStyle wenv config = style where
theme = collectTheme wenv L.selectListItemStyle
style = fromJust (Just theme <> _slcItemStyle config)
updateResultStyle
:: WidgetEnv s e
-> SelectListCfg s e a
-> WidgetResult s e
-> SelectListState a
-> SelectListState a
-> WidgetResult s e
updateResultStyle wenv config result oldState newState = newResult where
slIdx = _slIdx newState
hlIdx = _hlIdx newState
tmpNode = result ^. L.node
(newNode, reqs) = updateStyles wenv config oldState tmpNode slIdx hlIdx
newResult = resultReqs newNode reqs
makeItemsList
:: (WidgetModel s, WidgetEvent e, Eq a)
=> WidgetEnv s e
-> Seq a
-> SelectListMakeRow s e a
-> SelectListCfg s e a
-> WidgetId
-> a
-> WidgetNode s e
makeItemsList wenv items makeRow config widgetId selected = itemsList where
normalTheme = collectTheme wenv L.selectListItemStyle
normalStyle = fromJust (Just normalTheme <> _slcItemStyle config)
makeItem idx item = newItem where
clickCfg = onClickReq $ SendMessage widgetId (SelectListClickItem idx)
itemCfg = [expandContent, clickCfg]
content = makeRow item
newItem = box_ itemCfg (content & L.info . L.style .~ normalStyle)
itemsList = vstack $ Seq.mapWithIndex makeItem items