monomer-1.0.0.3: src/Monomer/Widgets/Util/Style.hs
{-|
Module : Monomer.Widgets.Util.Style
Copyright : (c) 2018 Francisco Vallarino
License : BSD-3-Clause (see the LICENSE file)
Maintainer : fjvallarino@gmail.com
Stability : experimental
Portability : non-portable
Helper functions for style related operations.
-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE Strict #-}
module Monomer.Widgets.Util.Style (
collectStyleField,
collectStyleField_,
currentTheme,
currentTheme_,
currentStyle,
currentStyle_,
focusedStyle,
styleStateChanged,
initNodeStyle,
mergeBasicStyle,
handleStyleChange,
childOfFocusedStyle
) where
import Control.Applicative ((<|>))
import Control.Lens (Lens', (&), (^.), (^?), (.~), (?~), (<>~), _Just, _1, non)
import Data.Bits (xor)
import Data.Default
import Data.Maybe
import Data.Sequence (Seq(..), (<|), (|>))
import qualified Data.Sequence as Seq
import Monomer.Core
import Monomer.Event
import Monomer.Helper
import Monomer.Widgets.Util.Focus
import Monomer.Widgets.Util.Hover
import Monomer.Widgets.Util.Types
import Monomer.Widgets.Util.Widget
import qualified Monomer.Core.Lens as L
import qualified Monomer.Event.Lens as L
instance Default (CurrentStyleCfg s e) where
def = CurrentStyleCfg {
_ascIsHovered = isNodeHovered,
_ascIsFocused = isNodeFocused,
_ascIsActive = isNodeActive
}
-- | Extracts/copies the field of a style into an empty style.
collectStyleField
:: Lens' StyleState (Maybe t) -- ^ The field into the state.
-> Style -- ^ The source style.
-> Style -- ^ The new style.
collectStyleField fieldS source = collectStyleField_ fieldS source def
-- | Extracts/copies the field of a style into a provided style.
collectStyleField_
:: Lens' StyleState (Maybe t) -- ^ The field into the state.
-> Style -- ^ The source style.
-> Style -- ^ The target style.
-> Style -- ^ The updated style.
collectStyleField_ fieldS source target = style where
setValue stateLens = result where
sourceState = source ^. stateLens
targetState = target ^. stateLens
value = sourceState ^? _Just . fieldS . _Just
setTarget val = targetState ^. non def
& fieldS ?~ val
resetTarget = targetState ^. non def
& fieldS .~ Nothing
result
| isJust value = setTarget <$> value
| isJust targetState = Just resetTarget
| otherwise = Nothing
basic = setValue L.basic
hover = setValue L.hover
focus = setValue L.focus
focusHover = setValue L.focusHover
active = setValue L.active
disabled = setValue L.disabled
style = Style basic hover focus focusHover active disabled
-- | Returns the current style state for the given node.
currentStyle :: WidgetEnv s e -> WidgetNode s e -> StyleState
currentStyle wenv node = currentStyle_ def wenv node
{-|
Returns the current style state for the given node, using the provided functions
to determine hover, focus and active status.
-}
currentStyle_
:: CurrentStyleCfg s e -> WidgetEnv s e -> WidgetNode s e -> StyleState
currentStyle_ config wenv node = fromMaybe def styleState where
Style{..} = node ^. L.info . L.style
mousePos = wenv ^. L.inputStatus . L.mousePos
isEnabled = node ^. L.info . L.enabled
isHover = _ascIsHovered config wenv node
isFocus = _ascIsFocused config wenv node
isActive = _ascIsActive config wenv node
styleState
| not isEnabled = _styleDisabled
| isActive = _styleActive
| isHover && isFocus = _styleFocusHover
| isHover = _styleHover
| isFocus = _styleFocus
| otherwise = _styleBasic
-- | Returns the correct focused style, depending if it's hovered or not.
focusedStyle :: WidgetEnv s e -> WidgetNode s e -> StyleState
focusedStyle wenv node = focusedStyle_ isNodeHovered wenv node
{-|
Returns the correct focused style, depending if it's hovered or not, using the
provided function.
-}
focusedStyle_ :: IsHovered s e -> WidgetEnv s e -> WidgetNode s e -> StyleState
focusedStyle_ isHoveredFn wenv node = fromMaybe def styleState where
Style{..} = node ^. L.info . L.style
isHover = isHoveredFn wenv node
styleState
| isHover = _styleFocusHover
| otherwise = _styleFocus
-- | Returns the current theme for the node.
currentTheme :: WidgetEnv s e -> WidgetNode s e -> ThemeState
currentTheme wenv node = currentTheme_ isNodeHovered wenv node
-- | Returns the current theme for the node.
currentTheme_ :: IsHovered s e -> WidgetEnv s e -> WidgetNode s e -> ThemeState
currentTheme_ isHoveredFn wenv node = themeState where
theme = _weTheme wenv
mousePos = wenv ^. L.inputStatus . L.mousePos
isEnabled = node ^. L.info . L.enabled
isHover = isHoveredFn wenv node
isFocus = isNodeFocused wenv node
isActive = isNodeActive wenv node
themeState
| not isEnabled = _themeDisabled theme
| isActive = _themeActive theme
| isHover && isFocus = _themeFocusHover theme
| isHover = _themeHover theme
| isFocus = _themeFocus theme
| otherwise = _themeBasic theme
-- | Checks if hover or focus states changed between versions of the node.
styleStateChanged :: WidgetEnv s e -> WidgetNode s e -> SystemEvent -> Bool
styleStateChanged wenv node evt = hoverChanged || focusChanged where
-- Hover
hoverChanged = isOnEnter evt || isOnLeave evt
-- Focus
focusChanged = isOnFocus evt || isOnBlur evt
{-|
Initializes the node style states. Mainly, it uses basic as the base of all the
other styles.
-}
initNodeStyle
:: GetBaseStyle s e -- ^ The function to get the base style.
-> WidgetEnv s e -- ^ The widget environment.
-> WidgetNode s e -- ^ The widget node.
-> WidgetNode s e -- ^ The updated widget node.
initNodeStyle getBaseStyle wenv node = newNode where
nodeStyle = mergeBasicStyle $ node ^. L.info . L.style
baseStyle = mergeBasicStyle $ fromMaybe def (getBaseStyle wenv node)
newNode = node
& L.info . L.style .~ (baseStyle <> nodeStyle)
-- | Uses the basic style state as the base for all the other style states.
mergeBasicStyle :: Style -> Style
mergeBasicStyle st = newStyle where
focusHover = _styleHover st <> _styleFocus st <> _styleFocusHover st
active = focusHover <> _styleActive st
newStyle = Style {
_styleBasic = _styleBasic st,
_styleHover = _styleBasic st <> _styleHover st,
_styleFocus = _styleBasic st <> _styleFocus st,
_styleFocusHover = _styleBasic st <> focusHover,
_styleActive = _styleBasic st <> active,
_styleDisabled = _styleBasic st <> _styleDisabled st
}
{-|
Checks for style changes between the old node and the provided result, in the
context of an event. Generates requests for resize, render and cursor change as
necessary.
-}
handleStyleChange
:: WidgetEnv s e -- ^ The widget environment.
-> Path -- ^ The target of the event.
-> StyleState -- ^ The active style.
-> Bool -- ^ Whether to check/update the cursor.
-> WidgetNode s e -- ^ The old node.
-> SystemEvent -- ^ The event.
-> Maybe (WidgetResult s e) -- ^ The result containing the new node.
-> Maybe (WidgetResult s e) -- ^ The updated result.
handleStyleChange wenv target style doCursor node evt result = newResult where
tmpResult = handleSizeChange wenv target evt node result
newResult
| doCursor = handleCursorChange wenv target evt style node tmpResult
| otherwise = tmpResult
{-|
Replacement of currentStyle for child widgets embedded in a focusable parent. It
selects the correct style state according to the situation.
Used, for example, in "Monomer.Widgets.Singles.Button" and
"Monomer.Widgets.Singles.ExternalLink", which are focusable but have an embedded
label. Since label is not focusable, that style would not be handled correctly.
-}
childOfFocusedStyle
:: WidgetEnv s e -- ^ The widget environment.
-> WidgetNode s e -- ^ The embedded child node.
-> StyleState -- ^ The currently active state.
childOfFocusedStyle wenv cnode = newStyle where
pinfo = fromMaybe def (wenv ^. L.findByPath $ parentPath cnode)
cstyle = cnode ^. L.info . L.style
enabled = cnode ^. L.info . L.enabled
activeC = isNodeActive wenv cnode
activeP = isNodeInfoActive False wenv pinfo
hoverC = isNodeHovered wenv cnode
hoverP = isNodeInfoHovered wenv pinfo
focusP = isNodeInfoFocused wenv pinfo
newStyle
| not enabled = fromMaybe def (_styleDisabled cstyle)
| activeC || activeP = fromMaybe def (_styleActive cstyle)
| (hoverC || hoverP) && focusP = fromMaybe def (_styleFocusHover cstyle)
| hoverC || hoverP = fromMaybe def (_styleHover cstyle)
| focusP = fromMaybe def (_styleFocus cstyle)
| otherwise = currentStyle wenv cnode
-- Helpers
handleSizeChange
:: WidgetEnv s e
-> Path
-> SystemEvent
-> WidgetNode s e
-> Maybe (WidgetResult s e)
-> Maybe (WidgetResult s e)
handleSizeChange wenv target evt oldNode result = newResult where
baseResult = fromMaybe (resultNode oldNode) result
newNode = baseResult ^. L.node
widgetId = newNode ^. L.info . L.widgetId
path = newNode ^. L.info . L.path
-- Size
oldSizeReqW = oldNode ^. L.info . L.sizeReqW
oldSizeReqH = oldNode ^. L.info . L.sizeReqH
newSizeReqW = newNode ^. L.info . L.sizeReqW
newSizeReqH = newNode ^. L.info . L.sizeReqH
sizeReqChanged = oldSizeReqW /= newSizeReqW || oldSizeReqH /= newSizeReqH
-- Hover drag changed (if dragging, Enter/Leave is not sent)
prevInVp = isPointInNodeVp newNode (wenv ^. L.inputStatus . L.mousePosPrev)
currInVp = isPointInNodeVp newNode (wenv ^. L.inputStatus . L.mousePos)
pressedPath = wenv ^. L.mainBtnPress ^? _Just . _1
hoverDragChg = Just path == pressedPath && prevInVp /= currInVp
-- Result
renderReq = isOnEnter evt || isOnLeave evt || hoverDragChg
resizeReq = [ ResizeWidgets widgetId | sizeReqChanged ]
enterReq = [ RenderOnce | renderReq ]
reqs = resizeReq ++ enterReq
newResult
| not (null reqs) = Just $ baseResult
& L.requests <>~ Seq.fromList reqs
| otherwise = result
handleCursorChange
:: WidgetEnv s e
-> Path
-> SystemEvent
-> StyleState
-> WidgetNode s e
-> Maybe (WidgetResult s e)
-> Maybe (WidgetResult s e)
handleCursorChange wenv target evt style oldNode result = newResult where
baseResult = fromMaybe (resultNode oldNode) result
baseReqs = baseResult ^. L.requests
node = baseResult ^. L.node
-- Cursor
widgetId = node ^. L.info . L.widgetId
path = node ^. L.info . L.path
isTarget = path == target
hasCursor = isJust (style ^. L.cursorIcon)
isPressed = isNodePressed wenv node
(curPath, curIcon) = fromMaybe def (wenv ^. L.cursor)
isParent = seqStartsWith path curPath && path /= curPath
newIcon = fromMaybe CursorArrow (style ^. L.cursorIcon)
setCursor = hasCursor
&& isCursorEvt evt
&& not isParent
&& curIcon /= newIcon
resetCursor = isTarget
&& not hasCursor
&& isCursorEvt evt
&& not isPressed
&& curPath == path
-- Result
newResult
| setCursor = Just $ baseResult
& L.requests .~ SetCursorIcon widgetId newIcon <| baseReqs
| resetCursor = Just $ baseResult
& L.requests .~ baseReqs |> ResetCursorIcon widgetId
| otherwise = result
isCursorEvt :: SystemEvent -> Bool
isCursorEvt Enter{} = True
isCursorEvt Click{} = True
isCursorEvt ButtonAction{} = True
isCursorEvt Move{} = True
isCursorEvt _ = False