{-| Module : Monomer.Widgets.Singles.Slider Copyright : (c) 2018 Francisco Vallarino License : BSD-3-Clause (see the LICENSE file) Maintainer : fjvallarino@gmail.com Stability : experimental Portability : non-portable Slider widget, used for interacting with numeric values. It allows changing the value using the keyboard arrows, dragging the mouse or using the wheel. @ hslider numericLens 0 100 @ Similar in objective to "Monomer.Widgets.Singles.Dial", but more convenient in some layouts. -} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StrictData #-} module Monomer.Widgets.Singles.Slider ( -- * Configuration SliderValue, SliderCfg, -- * Constructors hslider, hslider_, vslider, vslider_, hsliderV, hsliderV_, vsliderV, vsliderV_, sliderD_ ) 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, typeOf) import GHC.Generics import TextShow 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 the slider widget. type SliderValue a = (Eq a, Show a, Real a, FromFractional a, Typeable a) {-| Configuration options for slider: - 'width': sets the size of the secondary axis of the Slider. - 'radius': the radius of the corners of the Slider. - 'wheelRate': The rate at which wheel movement affects the number. - 'dragRate': The rate at which drag movement affects the number. - 'thumbVisible': whether a thumb should be visible or not. - 'thumbFactor': the size of the thumb relative to width. - '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 SliderCfg s e a = SliderCfg { _slcRadius :: Maybe Double, _slcWidth :: Maybe Double, _slcWheelRate :: Maybe Rational, _slcDragRate :: Maybe Rational, _slcThumbVisible :: Maybe Bool, _slcThumbFactor :: Maybe Double, _slcOnFocusReq :: [Path -> WidgetRequest s e], _slcOnBlurReq :: [Path -> WidgetRequest s e], _slcOnChangeReq :: [a -> WidgetRequest s e] } instance Default (SliderCfg s e a) where def = SliderCfg { _slcRadius = Nothing, _slcWidth = Nothing, _slcWheelRate = Nothing, _slcDragRate = Nothing, _slcThumbVisible = Nothing, _slcThumbFactor = Nothing, _slcOnFocusReq = [], _slcOnBlurReq = [], _slcOnChangeReq = [] } instance Semigroup (SliderCfg s e a) where (<>) t1 t2 = SliderCfg { _slcRadius = _slcRadius t2 <|> _slcRadius t1, _slcWidth = _slcWidth t2 <|> _slcWidth t1, _slcWheelRate = _slcWheelRate t2 <|> _slcWheelRate t1, _slcDragRate = _slcDragRate t2 <|> _slcDragRate t1, _slcThumbVisible = _slcThumbVisible t2 <|> _slcThumbVisible t1, _slcThumbFactor = _slcThumbFactor t2 <|> _slcThumbFactor t1, _slcOnFocusReq = _slcOnFocusReq t1 <> _slcOnFocusReq t2, _slcOnBlurReq = _slcOnBlurReq t1 <> _slcOnBlurReq t2, _slcOnChangeReq = _slcOnChangeReq t1 <> _slcOnChangeReq t2 } instance Monoid (SliderCfg s e a) where mempty = def instance CmbWidth (SliderCfg s e a) where width w = def { _slcWidth = Just w } instance CmbRadius (SliderCfg s e a) where radius w = def { _slcRadius = Just w } instance CmbWheelRate (SliderCfg s e a) Rational where wheelRate rate = def { _slcWheelRate = Just rate } instance CmbDragRate (SliderCfg s e a) Rational where dragRate rate = def { _slcDragRate = Just rate } instance CmbThumbFactor (SliderCfg s e a) where thumbFactor w = def { _slcThumbFactor = Just w } instance CmbThumbVisible (SliderCfg s e a) where thumbVisible_ w = def { _slcThumbVisible = Just w } instance WidgetEvent e => CmbOnFocus (SliderCfg s e a) e Path where onFocus fn = def { _slcOnFocusReq = [RaiseEvent . fn] } instance CmbOnFocusReq (SliderCfg s e a) s e Path where onFocusReq req = def { _slcOnFocusReq = [req] } instance WidgetEvent e => CmbOnBlur (SliderCfg s e a) e Path where onBlur fn = def { _slcOnBlurReq = [RaiseEvent . fn] } instance CmbOnBlurReq (SliderCfg s e a) s e Path where onBlurReq req = def { _slcOnBlurReq = [req] } instance WidgetEvent e => CmbOnChange (SliderCfg s e a) a e where onChange fn = def { _slcOnChangeReq = [RaiseEvent . fn] } instance CmbOnChangeReq (SliderCfg s e a) s e a where onChangeReq req = def { _slcOnChangeReq = [req] } data SliderState = SliderState { _slsMaxPos :: Integer, _slsPos :: Integer } deriving (Eq, Show, Generic) {-| Creates a horizontal slider using the given lens, providing minimum and maximum values. -} hslider :: (SliderValue a, WidgetEvent e) => ALens' s a -> a -> a -> WidgetNode s e hslider field minVal maxVal = hslider_ field minVal maxVal def {-| Creates a horizontal slider using the given lens, providing minimum and maximum values. Accepts config. -} hslider_ :: (SliderValue a, WidgetEvent e) => ALens' s a -> a -> a -> [SliderCfg s e a] -> WidgetNode s e hslider_ field minVal maxVal cfg = sliderD_ True wlens minVal maxVal cfg where wlens = WidgetLens field {-| Creates a vertical slider using the given lens, providing minimum and maximum values. -} vslider :: (SliderValue a, WidgetEvent e) => ALens' s a -> a -> a -> WidgetNode s e vslider field minVal maxVal = vslider_ field minVal maxVal def {-| Creates a vertical slider using the given lens, providing minimum and maximum values. Accepts config. -} vslider_ :: (SliderValue a, WidgetEvent e) => ALens' s a -> a -> a -> [SliderCfg s e a] -> WidgetNode s e vslider_ field minVal maxVal cfg = sliderD_ False wlens minVal maxVal cfg where wlens = WidgetLens field {-| Creates a horizontal slider using the given value and 'onChange' event handler, providing minimum and maximum values. -} hsliderV :: (SliderValue a, WidgetEvent e) => a -> (a -> e) -> a -> a -> WidgetNode s e hsliderV value handler minVal maxVal = hsliderV_ value handler minVal maxVal def {-| Creates a horizontal slider using the given value and 'onChange' event handler, providing minimum and maximum values. Accepts config. -} hsliderV_ :: (SliderValue a, WidgetEvent e) => a -> (a -> e) -> a -> a -> [SliderCfg s e a] -> WidgetNode s e hsliderV_ value handler minVal maxVal configs = newNode where widgetData = WidgetValue value newConfigs = onChange handler : configs newNode = sliderD_ True widgetData minVal maxVal newConfigs {-| Creates a vertical slider using the given value and 'onChange' event handler, providing minimum and maximum values. -} vsliderV :: (SliderValue a, WidgetEvent e) => a -> (a -> e) -> a -> a -> WidgetNode s e vsliderV value handler minVal maxVal = vsliderV_ value handler minVal maxVal def {-| Creates a vertical slider using the given value and 'onChange' event handler, providing minimum and maximum values. Accepts config. -} vsliderV_ :: (SliderValue a, WidgetEvent e) => a -> (a -> e) -> a -> a -> [SliderCfg s e a] -> WidgetNode s e vsliderV_ value handler minVal maxVal configs = newNode where widgetData = WidgetValue value newConfigs = onChange handler : configs newNode = sliderD_ False widgetData minVal maxVal newConfigs {-| Creates a slider providing direction, a 'WidgetData' instance, minimum and maximum values and config. -} sliderD_ :: (SliderValue a, WidgetEvent e) => Bool -> WidgetData s a -> a -> a -> [SliderCfg s e a] -> WidgetNode s e sliderD_ isHz widgetData minVal maxVal configs = sliderNode where config = mconcat configs state = SliderState 0 0 wtype = WidgetType ("slider-" <> showt (typeOf minVal)) widget = makeSlider isHz widgetData minVal maxVal config state sliderNode = defaultWidgetNode wtype widget & L.info . L.focusable .~ True makeSlider :: (SliderValue a, WidgetEvent e) => Bool -> WidgetData s a -> a -> a -> SliderCfg s e a -> SliderState -> Widget s e makeSlider !isHz !field !minVal !maxVal !config !state = widget where widget = createSingle state def { singleFocusOnBtnPressed = False, singleGetBaseStyle = getBaseStyle, singleInit = init, singleMerge = merge, singleHandleEvent = handleEvent, singleGetSizeReq = getSizeReq, singleRender = render } dragRate | isJust (_slcDragRate config) = fromJust (_slcDragRate config) | otherwise = toRational (maxVal - minVal) / 1000 getBaseStyle wenv node = Just style where style = collectTheme wenv L.sliderStyle init wenv node = resultNode resNode where currVal = widgetDataGet (wenv ^. L.model) field newState = newStateFromValue currVal resNode = node & L.widget .~ makeSlider isHz field minVal maxVal config newState merge wenv newNode oldNode oldState = resultNode resNode where stateVal = valueFromPos (_slsPos oldState) modelVal = widgetDataGet (wenv ^. L.model) field newState | isNodePressed wenv newNode = oldState | stateVal == modelVal = oldState | otherwise = newStateFromValue modelVal resNode = newNode & L.widget .~ makeSlider isHz field minVal maxVal config newState handleEvent wenv node target evt = case evt of Focus prev -> handleFocusChange node prev (_slcOnFocusReq config) Blur next -> handleFocusChange node next (_slcOnBlurReq config) KeyAction mod code KeyPressed | ctrlPressed && isInc code -> handleNewPos (pos + warpSpeed) | ctrlPressed && isDec code -> handleNewPos (pos - warpSpeed) | shiftPressed && isInc code -> handleNewPos (pos + baseSpeed) | shiftPressed && isDec code -> handleNewPos (pos - baseSpeed) | isInc code -> handleNewPos (pos + fastSpeed) | isDec code -> handleNewPos (pos - fastSpeed) where ctrlPressed = isShortCutControl wenv mod (isDec, isInc) | isHz = (isKeyLeft, isKeyRight) | otherwise = (isKeyDown, isKeyUp) baseSpeed = max 1 $ round (fromIntegral maxPos / 1000) fastSpeed = max 1 $ round (fromIntegral maxPos / 100) warpSpeed = max 1 $ round (fromIntegral maxPos / 10) handleNewPos !newPos | validPos /= pos = resultFromPos validPos [] | otherwise = Nothing where validPos = clamp 0 maxPos newPos Move point | isNodePressed wenv node -> resultFromPoint point [] ButtonAction point btn BtnPressed clicks -> resultFromPoint point reqs where reqs | shiftPressed = [] | otherwise = [SetFocus widgetId] ButtonAction point btn BtnReleased clicks -> resultFromPoint point [] WheelScroll _ (Point _ wy) wheelDirection -> resultFromPos newPos reqs where wheelCfg = fromMaybe (theme ^. L.sliderWheelRate) (_slcWheelRate config) wheelRate = fromRational wheelCfg tmpPos = pos + round (wy * wheelRate) newPos = clamp 0 maxPos tmpPos reqs = [IgnoreParentEvents] _ -> Nothing where theme = currentTheme wenv node style = currentStyle wenv node vp = getContentArea node style widgetId = node ^. L.info . L.widgetId shiftPressed = wenv ^. L.inputStatus . L.keyMod . L.leftShift SliderState maxPos pos = state resultFromPoint !point !reqs = resultFromPos newPos reqs where !newPos = posFromMouse isHz vp point resultFromPos !newPos !extraReqs = Just newResult where !newState = state { _slsPos = newPos } !newNode = node & L.widget .~ makeSlider isHz field minVal maxVal config newState !result = resultReqs newNode [RenderOnce] !newVal = valueFromPos newPos !reqs = widgetDataSet field newVal ++ fmap ($ newVal) (_slcOnChangeReq config) ++ extraReqs !newResult | pos /= newPos = result & L.requests <>~ Seq.fromList reqs | otherwise = result getSizeReq wenv node = req where theme = currentTheme wenv node maxPos = realToFrac (toRational (maxVal - minVal) / dragRate) width = fromMaybe (theme ^. L.sliderWidth) (_slcWidth config) req | isHz = (expandSize maxPos 1, fixedSize width) | otherwise = (fixedSize width, expandSize maxPos 1) render wenv node renderer = do drawRect renderer sliderBgArea (Just sndColor) sliderRadius drawInScissor renderer True sliderFgArea $ drawRect renderer sliderBgArea (Just fgColor) sliderRadius when thbVisible $ drawEllipse renderer thbArea (Just hlColor) where theme = currentTheme wenv node style = currentStyle wenv node fgColor = styleFgColor style hlColor = styleHlColor style sndColor = styleSndColor style radiusW = _slcRadius config <|> theme ^. L.sliderRadius sliderRadius = radius <$> radiusW SliderState maxPos pos = state posPct = fromIntegral pos / fromIntegral maxPos carea = getContentArea node style Rect cx cy cw ch = carea barW | isHz = ch | otherwise = cw -- Thumb thbVisible = fromMaybe False (_slcThumbVisible config) thbF = fromMaybe (theme ^. L.sliderThumbFactor) (_slcThumbFactor config) thbW = thbF * barW thbPos dim = (dim - thbW) * posPct thbDif = (thbW - barW) / 2 thbArea | isHz = Rect (cx + thbPos cw) (cy - thbDif) thbW thbW | otherwise = Rect (cx - thbDif) (cy + ch - thbW - thbPos ch) thbW thbW -- Bar tw2 = thbW / 2 sliderBgArea | not thbVisible = carea | isHz = fromMaybe def (subtractFromRect carea tw2 tw2 0 0) | otherwise = fromMaybe def (subtractFromRect carea 0 0 tw2 tw2) sliderFgArea | isHz = sliderBgArea & L.w %~ (*posPct) | otherwise = sliderBgArea & L.y %~ (+ (sliderBgArea ^. L.h * (1 - posPct))) & L.h %~ (*posPct) newStateFromValue currVal = newState where newMaxPos = round (toRational (maxVal - minVal) / dragRate) newPos = round (toRational (currVal - minVal) / dragRate) newState = SliderState { _slsMaxPos = newMaxPos, _slsPos = newPos } posFromMouse isHz vp point = newPos where SliderState maxPos _ = state dv | isHz = point ^. L.x - vp ^. L.x | otherwise = vp ^. L.y + vp ^. L.h - point ^. L.y tmpPos | isHz = round (dv * fromIntegral maxPos / vp ^. L.w) | otherwise = round (dv * fromIntegral maxPos / vp ^. L.h) newPos = clamp 0 maxPos tmpPos valueFromPos newPos = newVal where newVal = minVal + fromFractional (dragRate * fromIntegral newPos)