monomer-1.4.0.0: src/Monomer/Widgets/Singles/Button.hs
{-|
Module : Monomer.Widgets.Singles.Button
Copyright : (c) 2018 Francisco Vallarino
License : BSD-3-Clause (see the LICENSE file)
Maintainer : fjvallarino@gmail.com
Stability : experimental
Portability : non-portable
Button widget, with support for multiline text. At the most basic level, a
button consists of a caption and an event to raise when clicked.
@
button "Increase count" AppIncrease
@
-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE StrictData #-}
module Monomer.Widgets.Singles.Button (
-- * Configuration
ButtonCfg,
-- * Constructors
mainButton,
mainButton_,
mainButtonD_,
button,
button_,
buttonD_
) where
import Control.Applicative ((<|>))
import Control.Lens ((&), (^.), (.~))
import Data.Default
import Data.Maybe
import Data.Text (Text)
import qualified Data.Sequence as Seq
import Monomer.Widgets.Container
import Monomer.Widgets.Singles.Label
import qualified Monomer.Lens as L
data ButtonType
= ButtonNormal
| ButtonMain
deriving (Eq, Show)
{-|
Configuration options for button:
- 'ignoreParentEvts': whether to ignore all other responses to the click or
keypress that triggered the button, and only keep this button's response.
Useful when the button is child of a _keystroke_ widget.
- 'trimSpaces': whether to remove leading/trailing spaces in the caption.
- 'ellipsis': if ellipsis should be used for overflown text.
- 'multiline': if text may be split in multiple lines.
- 'maxLines': maximum number of text lines to show.
- 'resizeFactor': flexibility to have more or less spaced assigned.
- 'resizeFactorW': flexibility to have more or less horizontal spaced assigned.
- 'resizeFactorH': flexibility to have more or less vertical spaced assigned.
- '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.
- 'onClick': event to raise when button is clicked.
- 'onClickReq': 'WidgetRequest' to generate when button is clicked.
-}
data ButtonCfg s e = ButtonCfg {
_btnButtonType :: Maybe ButtonType,
_btnIgnoreParent :: Maybe Bool,
_btnIgnoreTheme :: Maybe Bool,
_btnLabelCfg :: LabelCfg s e,
_btnOnFocusReq :: [Path -> WidgetRequest s e],
_btnOnBlurReq :: [Path -> WidgetRequest s e],
_btnOnClickReq :: [WidgetRequest s e]
}
instance Default (ButtonCfg s e) where
def = ButtonCfg {
_btnButtonType = Nothing,
_btnIgnoreParent = Nothing,
_btnIgnoreTheme = Nothing,
_btnLabelCfg = def,
_btnOnFocusReq = [],
_btnOnBlurReq = [],
_btnOnClickReq = []
}
instance Semigroup (ButtonCfg s e) where
(<>) t1 t2 = ButtonCfg {
_btnButtonType = _btnButtonType t2 <|> _btnButtonType t1,
_btnIgnoreParent = _btnIgnoreParent t2 <|> _btnIgnoreParent t1,
_btnIgnoreTheme = _btnIgnoreTheme t2 <|> _btnIgnoreTheme t1,
_btnLabelCfg = _btnLabelCfg t1 <> _btnLabelCfg t2,
_btnOnFocusReq = _btnOnFocusReq t1 <> _btnOnFocusReq t2,
_btnOnBlurReq = _btnOnBlurReq t1 <> _btnOnBlurReq t2,
_btnOnClickReq = _btnOnClickReq t1 <> _btnOnClickReq t2
}
instance Monoid (ButtonCfg s e) where
mempty = def
instance CmbIgnoreParentEvts (ButtonCfg s e) where
ignoreParentEvts_ ignore = def {
_btnIgnoreParent = Just ignore
}
instance CmbIgnoreTheme (ButtonCfg s e) where
ignoreTheme_ ignore = def {
_btnIgnoreTheme = Just ignore
}
instance CmbTrimSpaces (ButtonCfg s e) where
trimSpaces_ trim = def {
_btnLabelCfg = trimSpaces_ trim
}
instance CmbEllipsis (ButtonCfg s e) where
ellipsis_ ellipsis = def {
_btnLabelCfg = ellipsis_ ellipsis
}
instance CmbMultiline (ButtonCfg s e) where
multiline_ multi = def {
_btnLabelCfg = multiline_ multi
}
instance CmbMaxLines (ButtonCfg s e) where
maxLines count = def {
_btnLabelCfg = maxLines count
}
instance CmbResizeFactor (ButtonCfg s e) where
resizeFactor s = def {
_btnLabelCfg = resizeFactor s
}
instance CmbResizeFactorDim (ButtonCfg s e) where
resizeFactorW w = def {
_btnLabelCfg = resizeFactorW w
}
resizeFactorH h = def {
_btnLabelCfg = resizeFactorH h
}
instance WidgetEvent e => CmbOnFocus (ButtonCfg s e) e Path where
onFocus fn = def {
_btnOnFocusReq = [RaiseEvent . fn]
}
instance CmbOnFocusReq (ButtonCfg s e) s e Path where
onFocusReq req = def {
_btnOnFocusReq = [req]
}
instance WidgetEvent e => CmbOnBlur (ButtonCfg s e) e Path where
onBlur fn = def {
_btnOnBlurReq = [RaiseEvent . fn]
}
instance CmbOnBlurReq (ButtonCfg s e) s e Path where
onBlurReq req = def {
_btnOnBlurReq = [req]
}
instance WidgetEvent e => CmbOnClick (ButtonCfg s e) e where
onClick handler = def {
_btnOnClickReq = [RaiseEvent handler]
}
instance CmbOnClickReq (ButtonCfg s e) s e where
onClickReq req = def {
_btnOnClickReq = [req]
}
mainConfig :: ButtonCfg s e
mainConfig = def {
_btnButtonType = Just ButtonMain
}
{-|
Creates a button with main styling. Useful to highlight an option, such as
"Accept", when multiple buttons are available.
-}
mainButton :: WidgetEvent e => Text -> e -> WidgetNode s e
mainButton caption handler = button_ caption handler [mainConfig]
{-|
Creates a button with main styling. Useful to highlight an option, such as
"Accept", when multiple buttons are available. Accepts config.
-}
mainButton_ :: WidgetEvent e => Text -> e -> [ButtonCfg s e] -> WidgetNode s e
mainButton_ caption handler configs = button_ caption handler newConfigs where
newConfigs = mainConfig : configs
{-|
Creates a button with main styling. Useful to highlight an option, such as
"Accept", when multiple buttons are available. Accepts config but does not
require an event. See 'buttonD_'.
-}
mainButtonD_ :: WidgetEvent e => Text -> [ButtonCfg s e] -> WidgetNode s e
mainButtonD_ caption configs = buttonD_ caption newConfigs where
newConfigs = mainConfig : configs
-- | Creates a button with normal styling.
button :: WidgetEvent e => Text -> e -> WidgetNode s e
button caption handler = button_ caption handler def
-- | Creates a button with normal styling. Accepts config.
button_ :: WidgetEvent e => Text -> e -> [ButtonCfg s e] -> WidgetNode s e
button_ caption handler configs = buttonNode where
buttonNode = buttonD_ caption (onClick handler : configs)
{-|
Creates a button without forcing an event to be provided. The other constructors
use this version, adding an 'onClick' handler in configs.
Using this constructor directly can be helpful in cases where the event to be
raised belongs in a _Composite_ above in the widget tree, outside the scope of
the Composite that contains the button. This parent Composite can be reached by
sending a message ('SendMessage') to its 'WidgetId' using 'onClickReq'.
-}
buttonD_ :: WidgetEvent e => Text -> [ButtonCfg s e] -> WidgetNode s e
buttonD_ caption configs = buttonNode where
config = mconcat configs
widget = makeButton caption config
!buttonNode = defaultWidgetNode "button" widget
& L.info . L.focusable .~ True
makeButton :: WidgetEvent e => Text -> ButtonCfg s e -> Widget s e
makeButton !caption !config = widget where
widget = createContainer () def {
containerAddStyleReq = False,
containerDrawDecorations = False,
containerUseScissor = True,
containerGetBaseStyle = getBaseStyle,
containerInit = init,
containerMerge = merge,
containerHandleEvent = handleEvent,
containerResize = resize
}
!buttonType = fromMaybe ButtonNormal (_btnButtonType config)
getBaseStyle wenv node
| ignoreTheme = Nothing
| otherwise = case buttonType of
ButtonNormal -> Just (collectTheme wenv L.btnStyle)
ButtonMain -> Just (collectTheme wenv L.btnMainStyle)
where
ignoreTheme = _btnIgnoreTheme config == Just True
createChildNode wenv node = newNode where
nodeStyle = node ^. L.info . L.style
labelCfg = _btnLabelCfg config
labelCurrStyle = labelCurrentStyle childOfFocusedStyle
!labelNode = label_ caption [ignoreTheme, labelCfg, labelCurrStyle]
& L.info . L.style .~ nodeStyle
!newNode = node
& L.children .~ Seq.singleton labelNode
init wenv node = result where
result = resultNode (createChildNode wenv node)
merge wenv node oldNode oldState = result where
result = resultNode (createChildNode wenv node)
handleEvent wenv node target evt = case evt of
Focus prev -> handleFocusChange node prev (_btnOnFocusReq config)
Blur next -> handleFocusChange node next (_btnOnBlurReq config)
KeyAction mode code status
| isSelectKey code && status == KeyPressed -> Just result
where
isSelectKey code = isKeyReturn code || isKeySpace code
Click p _ _
| isPointInNodeVp node p -> Just result
ButtonAction p btn BtnPressed 1 -- Set focus on click
| mainBtn btn && pointInVp p && not focused -> Just resultFocus
_ -> Nothing
where
mainBtn btn = btn == wenv ^. L.mainButton
focused = isNodeFocused wenv node
pointInVp p = isPointInNodeVp node p
ignoreParent = _btnIgnoreParent config == Just True
reqs = _btnOnClickReq config ++ [IgnoreParentEvents | ignoreParent]
result = resultReqs node reqs
resultFocus = resultReqs node [SetFocus (node ^. L.info . L.widgetId)]
resize wenv node viewport children = resized where
assignedAreas = Seq.fromList [viewport]
resized = (resultNode node, assignedAreas)