{-| Module : Monomer.Widgets.Singles.Dial Copyright : (c) 2018 Francisco Vallarino License : BSD-3-Clause (see the LICENSE file) Maintainer : fjvallarino@gmail.com Stability : experimental Portability : non-portable Dial widget, used for interacting with numeric values. It allows changing the value by keyboard arrows, dragging the mouse or using the wheel. Similar in objective to "Monomer.Widgets.Singles.Slider", but uses less space. -} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StrictData #-} module Monomer.Widgets.Singles.Dial ( -- * Configuration DialValue, DialCfg, -- * Constructors dial, dial_, dialV, dialV_ ) where import Control.Applicative ((<|>)) import Control.Lens (ALens', (&), (^.), (.~), (<>~)) import Control.Monad import Data.Default import Data.Maybe import Data.Text (Text) import Data.Typeable (Typeable) import GHC.Generics import qualified Data.Sequence as Seq import Monomer.Helper import Monomer.Widgets.Single import qualified Monomer.Lens as L -- | Constraints for numeric types accepted by dial. type DialValue a = (Eq a, Show a, Real a, FromFractional a, Typeable a) {-| Configuration options for dial: - 'width': sets the max width/height of the dial. - 'wheelRate': The rate at which wheel movement affects the number. - 'dragRate': The rate at which drag movement affects the number. - '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 the value changes. - 'onChangeReq': 'WidgetRequest' to generate when the value changes. -} data DialCfg s e a = DialCfg { _dlcWidth :: Maybe Double, _dlcWheelRate :: Maybe Rational, _dlcDragRate :: Maybe Rational, _dlcOnFocusReq :: [Path -> WidgetRequest s e], _dlcOnBlurReq :: [Path -> WidgetRequest s e], _dlcOnChangeReq :: [a -> WidgetRequest s e] } instance Default (DialCfg s e a) where def = DialCfg { _dlcWidth = Nothing, _dlcWheelRate = Nothing, _dlcDragRate = Nothing, _dlcOnFocusReq = [], _dlcOnBlurReq = [], _dlcOnChangeReq = [] } instance Semigroup (DialCfg s e a) where (<>) t1 t2 = DialCfg { _dlcWidth = _dlcWidth t2 <|> _dlcWidth t1, _dlcWheelRate = _dlcWheelRate t2 <|> _dlcWheelRate t1, _dlcDragRate = _dlcDragRate t2 <|> _dlcDragRate t1, _dlcOnFocusReq = _dlcOnFocusReq t1 <> _dlcOnFocusReq t2, _dlcOnBlurReq = _dlcOnBlurReq t1 <> _dlcOnBlurReq t2, _dlcOnChangeReq = _dlcOnChangeReq t1 <> _dlcOnChangeReq t2 } instance Monoid (DialCfg s e a) where mempty = def instance CmbWheelRate (DialCfg s e a) Rational where wheelRate rate = def { _dlcWheelRate = Just rate } instance CmbDragRate (DialCfg s e a) Rational where dragRate rate = def { _dlcDragRate = Just rate } instance CmbWidth (DialCfg s e a) where width w = def { _dlcWidth = Just w } instance WidgetEvent e => CmbOnFocus (DialCfg s e a) e Path where onFocus fn = def { _dlcOnFocusReq = [RaiseEvent . fn] } instance CmbOnFocusReq (DialCfg s e a) s e Path where onFocusReq req = def { _dlcOnFocusReq = [req] } instance WidgetEvent e => CmbOnBlur (DialCfg s e a) e Path where onBlur fn = def { _dlcOnBlurReq = [RaiseEvent . fn] } instance CmbOnBlurReq (DialCfg s e a) s e Path where onBlurReq req = def { _dlcOnBlurReq = [req] } instance WidgetEvent e => CmbOnChange (DialCfg s e a) a e where onChange fn = def { _dlcOnChangeReq = [RaiseEvent . fn] } instance CmbOnChangeReq (DialCfg s e a) s e a where onChangeReq req = def { _dlcOnChangeReq = [req] } data DialState = DialState { _dlsMaxPos :: Integer, _dlsPos :: Integer } deriving (Eq, Show, Generic) -- | Creates a dial using the given lens, providing minimum and maximum values. dial :: (DialValue a, WidgetEvent e) => ALens' s a -> a -> a -> WidgetNode s e dial field minVal maxVal = dial_ field minVal maxVal def {-| Creates a dial using the given lens, providing minimum and maximum values. Accepts config. -} dial_ :: (DialValue a, WidgetEvent e) => ALens' s a -> a -> a -> [DialCfg s e a] -> WidgetNode s e dial_ field minVal maxVal cfgs = dialD_ (WidgetLens field) minVal maxVal cfgs {-| Creates a dial using the given value and 'onChange' event handler, providing minimum and maximum values. -} dialV :: (DialValue a, WidgetEvent e) => a -> (a -> e) -> a -> a -> WidgetNode s e dialV value handler minVal maxVal = dialV_ value handler minVal maxVal def {-| Creates a dial using the given value and 'onChange' event handler, providing minimum and maximum values. Accepts config. -} dialV_ :: (DialValue a, WidgetEvent e) => a -> (a -> e) -> a -> a -> [DialCfg s e a] -> WidgetNode s e dialV_ value handler minVal maxVal configs = newNode where widgetData = WidgetValue value newConfigs = onChange handler : configs newNode = dialD_ widgetData minVal maxVal newConfigs {-| Creates a dial providing a 'WidgetData' instance, minimum and maximum values and config. -} dialD_ :: (DialValue a, WidgetEvent e) => WidgetData s a -> a -> a -> [DialCfg s e a] -> WidgetNode s e dialD_ widgetData minVal maxVal configs = dialNode where config = mconcat configs state = DialState 0 0 widget = makeDial widgetData minVal maxVal config state dialNode = defaultWidgetNode "dial" widget & L.info . L.focusable .~ True makeDial :: (DialValue a, WidgetEvent e) => WidgetData s a -> a -> a -> DialCfg s e a -> DialState -> Widget s e makeDial !field !minVal !maxVal !config !state = widget where widget = createSingle state def { singleFocusOnBtnPressed = False, singleGetBaseStyle = getBaseStyle, singleGetCurrentStyle = getCurrentStyle, singleInit = init, singleMerge = merge, singleFindByPoint = findByPoint, singleHandleEvent = handleEvent, singleGetSizeReq = getSizeReq, singleRender = render } dragRate | isJust (_dlcDragRate config) = fromJust (_dlcDragRate config) | otherwise = toRational (maxVal - minVal) / 1000 getBaseStyle wenv node = Just style where style = collectTheme wenv L.dialStyle getCurrentStyle wenv node = style where (_, dialArea) = getDialInfo wenv node config style = currentStyle_ (currentStyleConfig dialArea) wenv node init wenv node = resultNode resNode where newState = newStateFromModel wenv node state resNode = node & L.widget .~ makeDial field minVal maxVal config newState merge wenv newNode oldNode oldState = resultNode resNode where newState | isNodePressed wenv newNode = oldState | otherwise = newStateFromModel wenv newNode oldState resNode = newNode & L.widget .~ makeDial field minVal maxVal config newState findByPoint wenv node path point | isVisible && pointInEllipse point dialArea = Just wni | otherwise = Nothing where isVisible = node ^. L.info . L.visible wni = node ^. L.info (_, dialArea) = getDialInfo wenv node config handleEvent wenv node target evt = case evt of Focus prev -> handleFocusChange node prev (_dlcOnFocusReq config) Blur next -> handleFocusChange node next (_dlcOnBlurReq config) KeyAction mod code KeyPressed | ctrlPressed && isKeyUp code -> handleNewPos (pos + warpSpeed) | ctrlPressed && isKeyDown code -> handleNewPos (pos - warpSpeed) | shiftPressed && isKeyUp code -> handleNewPos (pos + baseSpeed) | shiftPressed && isKeyDown code -> handleNewPos (pos - baseSpeed) | isKeyUp code -> handleNewPos (pos + fastSpeed) | isKeyDown code -> handleNewPos (pos - fastSpeed) where DialState maxPos pos = state ctrlPressed = isShortCutControl wenv mod baseSpeed = max 1 $ round (fromIntegral maxPos / 1000) fastSpeed = max 1 $ round (fromIntegral maxPos / 100) warpSpeed = max 1 $ round (fromIntegral maxPos / 10) vPos pos = clamp 0 maxPos pos newResult !newPos = addReqsEvts (resultNode newNode) newVal where newVal = valueFromPos minVal dragRate newPos !newState = state { _dlsPos = newPos } !newNode = node & L.widget .~ makeDial field minVal maxVal config newState handleNewPos !newPos | vPos newPos /= pos = Just $ newResult (vPos newPos) | otherwise = Nothing Move point | isNodePressed wenv node -> Just result where (_, start) = fromJust $ wenv ^. L.mainBtnPress (_, newVal) = posFromPoint minVal maxVal state dragRate start point result = addReqsEvts (resultReqs node [RenderOnce]) newVal ButtonAction point btn BtnPressed clicks | not (isNodeFocused wenv node) && not shiftPressed -> Just result where result = resultReqs node [SetFocus widgetId] ButtonAction point btn BtnReleased clicks -> Just result where reqs = [RenderOnce] newState = newStateFromModel wenv node state newNode = node & L.widget .~ makeDial field minVal maxVal config newState result = resultReqs newNode reqs WheelScroll _ (Point _ wy) wheelDirection -> Just result where DialState maxPos pos = state wheelCfg = fromMaybe (theme ^. L.sliderWheelRate) (_dlcWheelRate config) wheelRate = fromRational wheelCfg tmpPos = pos + round (wy * wheelRate) newPos = clamp 0 maxPos tmpPos newVal = valueFromPos minVal dragRate newPos reqs = [RenderOnce, IgnoreParentEvents] result = addReqsEvts (resultReqs node reqs) newVal _ -> Nothing where theme = currentTheme wenv node (_, dialArea) = getDialInfo wenv node config widgetId = node ^. L.info . L.widgetId path = node ^. L.info . L.path shiftPressed = wenv ^. L.inputStatus . L.keyMod . L.leftShift isSelectKey code = isKeyReturn code || isKeySpace code addReqsEvts result newVal = newResult where currVal = widgetDataGet (wenv ^. L.model) field reqs = widgetDataSet field newVal ++ fmap ($ newVal) (_dlcOnChangeReq config) newResult | currVal /= newVal = result & L.requests <>~ Seq.fromList reqs | otherwise = result getSizeReq wenv node = req where theme = currentTheme wenv node width = fromMaybe (theme ^. L.dialWidth) (_dlcWidth config) req = (fixedSize width, fixedSize width) render wenv node renderer = do drawArcBorder renderer dialArea start endSnd CW (Just sndColor) dialBW drawArcBorder renderer dialArea start endFg CW (Just fgColor) dialBW where (dialCenter, dialArea) = getDialInfo wenv node config DialState maxPos pos = newStateFromModel wenv node state posPct = fromIntegral pos / fromIntegral maxPos dialBW = max 1 (_rW dialArea * 0.15) style = getCurrentStyle wenv node fgColor = styleFgColor style sndColor = styleSndColor style start = 90 + 45 endFg = start + 270 * posPct endSnd = 45 newStateFromModel wenv node oldState = newState where currVal = widgetDataGet (wenv ^. L.model) field newMaxPos = round (toRational (maxVal - minVal) / dragRate) newPos = round (toRational (currVal - minVal) / dragRate) newState = oldState { _dlsMaxPos = newMaxPos, _dlsPos = newPos } posFromPoint :: DialValue a => a -> a -> DialState -> Rational -> Point -> Point -> (Integer, a) posFromPoint minVal maxVal state dragRate stPoint point = (newPos, newVal) where DialState maxPos pos = state Point _ dy = subPoint stPoint point tmpPos = pos + round dy newPos = clamp 0 maxPos tmpPos newVal = valueFromPos minVal dragRate newPos valueFromPos :: DialValue a => a -> Rational -> Integer -> a valueFromPos !minVal !dragRate !newPos = newVal where newVal = minVal + fromFractional (dragRate * fromIntegral newPos) getDialInfo :: WidgetEnv s e -> WidgetNode s e -> DialCfg s e a -> (Point, Rect) getDialInfo wenv node config = (dialCenter, dialArea) where theme = currentTheme wenv node style = currentStyle wenv node carea = getContentArea node style dialW = fromMaybe (theme ^. L.dialWidth) (_dlcWidth config) dialL = _rX carea + (_rW carea - dialW) / 2 dialT = _rY carea + (_rH carea - dialW) / 2 !dialCenter = Point (dialL + dialW / 2) (dialT + dialW / 2) !dialArea = Rect dialL dialT dialW dialW currentStyleConfig :: Rect -> CurrentStyleCfg s e currentStyleConfig dialArea = def & L.isHovered .~ isNodeHoveredEllipse_ dialArea