packages feed

monomer-1.6.0.0: src/Monomer/Widgets/Singles/Slider.hs

{-|
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.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      -- ^ The lens into the model.
  -> a               -- ^ Minimum value.
  -> a               -- ^ Maximum value.
  -> WidgetNode s e  -- ^ The created slider.
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         -- ^ The lens into the model.
  -> a                  -- ^ Minimum value.
  -> a                  -- ^ Maximum value.
  -> [SliderCfg s e a]  -- ^ The config options.
  -> WidgetNode s e     -- ^ The created slider.
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      -- ^ The lens into the model.
  -> a               -- ^ Minimum value.
  -> a               -- ^ Maximum value.
  -> WidgetNode s e  -- ^ The created slider.
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         -- ^ The lens into the model.
  -> a                  -- ^ Minimum value.
  -> a                  -- ^ Maximum value.
  -> [SliderCfg s e a]  -- ^ The config options.
  -> WidgetNode s e     -- ^ The created slider.
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               -- ^ The current value.
  -> (a -> e)        -- ^ The event to raise on change.
  -> a               -- ^ Minimum value.
  -> a               -- ^ Maximum value.
  -> WidgetNode s e  -- ^ The created slider.
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                  -- ^ The current value.
  -> (a -> e)           -- ^ The event to raise on change.
  -> a                  -- ^ Minimum value.
  -> a                  -- ^ Maximum value.
  -> [SliderCfg s e a]  -- ^ The config options.
  -> WidgetNode s e     -- ^ The created slider.
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               -- ^ The current value.
  -> (a -> e)        -- ^ The event to raise on change.
  -> a               -- ^ Minimum value.
  -> a               -- ^ Maximum value.
  -> WidgetNode s e  -- ^ The created slider.
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                  -- ^ The current value.
  -> (a -> e)           -- ^ The event to raise on change.
  -> a                  -- ^ Minimum value.
  -> a                  -- ^ Maximum value.
  -> [SliderCfg s e a]  -- ^ The config options.
  -> WidgetNode s e     -- ^ The created slider.
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               -- ^ True if horizontal, False if vertical
  -> WidgetData s a     -- ^ The 'WidgetData' to retrieve the value from.
  -> a                  -- ^ Minimum value.
  -> a                  -- ^ Maximum value.
  -> [SliderCfg s e a]  -- ^ The config options.
  -> WidgetNode s e     -- ^ The created slider.
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)