{-| 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 by keyboard arrows, dragging the mouse or using the wheel. Similar in objective to Dial, but more convenient in some layouts. - 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. -} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE ScopedTypeVariables #-} module Monomer.Widgets.Singles.Slider ( 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) import GHC.Generics import qualified Data.Sequence as Seq import Monomer.Helper import Monomer.Widgets.Single import qualified Monomer.Lens as L -- | Constraints for the values handled by slider widget. type SliderValue a = (Eq a, Show a, Real a, FromFractional a, Typeable a) -- | Configuration options for slider widget. 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 widget = makeSlider isHz widgetData minVal maxVal config state sliderNode = defaultWidgetNode "slider" 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 [] where wheelCfg = fromMaybe (theme ^. L.sliderWheelRate) (_slcWheelRate config) wheelRate = fromRational wheelCfg tmpPos = pos + round (wy * wheelRate) newPos = clamp 0 maxPos tmpPos _ -> 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)