packages feed

monomer-1.5.1.0: src/Monomer/Widgets/Singles/ColorPopup.hs

{-|
Module      : Monomer.Widgets.Singles.ColorPopup
Copyright   : (c) 2018 Francisco Vallarino
License     : BSD-3-Clause (see the LICENSE file)
Maintainer  : fjvallarino@gmail.com
Stability   : experimental
Portability : non-portable

Color popup, displayed inside its parent container as a colored square. When
clicked, it opens a color picker overlay.

Shows sliders for the color components.

@
colorPopup colorLens
@

Optionally shows a slider for the alpha channel.

@
colorPopup_ colorLens [showAlpha]
@
-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE Strict #-}
{-# LANGUAGE TemplateHaskell #-}

module Monomer.Widgets.Singles.ColorPopup (
  -- * Constructors
  colorPopup,
  colorPopup_,
  colorPopupV,
  colorPopupV_
) where

import Control.Applicative ((<|>))
import Control.Lens ((&), (^.), (.~), (?~), ALens', abbreviatedFields, makeLensesWith, non)
import Data.Default
import Data.Text (Text)

import Monomer.Core.Combinators
import Monomer.Graphics.Types

import Monomer.Widgets.Composite
import Monomer.Widgets.Containers.BoxShadow
import Monomer.Widgets.Containers.Popup
import Monomer.Widgets.Containers.Stack
import Monomer.Widgets.Singles.ColorPicker
import Monomer.Widgets.Singles.ToggleButton

import qualified Monomer.Lens as L

type ColorPopupEnv = WidgetEnv ColorPopupModel ColorPopupEvt
type ColorPopupNode = WidgetNode ColorPopupModel ColorPopupEvt

{-|
Configuration options for colorPopup:

- 'showAlpha': whether to allow modifying the alpha channel or not.
- '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 any of the values changes.
- 'onChangeReq': 'WidgetRequest' to generate when any of the values changes.
-}
data ColorPopupCfg s e = ColorPopupCfg {
  _cpcColorPickerCfg :: ColorPickerCfg ColorPopupModel ColorPopupEvt,
  _cpcOnFocusReq :: [Path -> WidgetRequest s e],
  _cpcOnBlurReq :: [Path -> WidgetRequest s e],
  _cpcOnChangeReq :: [Color -> WidgetRequest s e]
}

instance Default (ColorPopupCfg s e) where
  def = ColorPopupCfg {
    _cpcColorPickerCfg = def,
    _cpcOnFocusReq = [],
    _cpcOnBlurReq = [],
    _cpcOnChangeReq = []
  }

instance Semigroup (ColorPopupCfg s e) where
  (<>) a1 a2 = def {
    _cpcColorPickerCfg = _cpcColorPickerCfg a1 <> _cpcColorPickerCfg a2,
    _cpcOnFocusReq = _cpcOnFocusReq a1 <> _cpcOnFocusReq a2,
    _cpcOnBlurReq = _cpcOnBlurReq a1 <> _cpcOnBlurReq a2,
    _cpcOnChangeReq = _cpcOnChangeReq a1 <> _cpcOnChangeReq a2
  }

instance Monoid (ColorPopupCfg s e) where
  mempty = def

instance CmbShowAlpha (ColorPopupCfg s e) where
  showAlpha_ show = def {
    _cpcColorPickerCfg = showAlpha_ show
  }

instance WidgetEvent e => CmbOnFocus (ColorPopupCfg s e) e Path where
  onFocus fn = def {
    _cpcOnFocusReq = [RaiseEvent . fn]
  }

instance CmbOnFocusReq (ColorPopupCfg s e) s e Path where
  onFocusReq req = def {
    _cpcOnFocusReq = [req]
  }

instance WidgetEvent e => CmbOnBlur (ColorPopupCfg s e) e Path where
  onBlur fn = def {
    _cpcOnBlurReq = [RaiseEvent . fn]
  }

instance CmbOnBlurReq (ColorPopupCfg s e) s e Path where
  onBlurReq req = def {
    _cpcOnBlurReq = [req]
  }

instance WidgetEvent e => CmbOnChange (ColorPopupCfg s e) Color e where
  onChange fn = def {
    _cpcOnChangeReq = [RaiseEvent . fn]
  }

instance CmbOnChangeReq (ColorPopupCfg s e) s e Color where
  onChangeReq req = def {
    _cpcOnChangeReq = [req]
  }

data ColorPopupModel = ColorPopupModel {
  _cpmPopupShowColor :: Bool,
  _cpmPopupColor :: Color
} deriving (Eq, Show)

data ColorPopupEvt
  = ColorChanged Color
  | PopupFocus Path
  | PopupBlur Path

instance Default ColorPopupModel where
  def = ColorPopupModel {
    _cpmPopupShowColor = False,
    _cpmPopupColor = def
  }

makeLensesWith abbreviatedFields 'ColorPopupModel

-- | Creates a colorPopup using the given lens.
colorPopup
  :: (WidgetModel s, WidgetEvent e)
  => ALens' s Color  -- ^ The lens into the model.
  -> WidgetNode s e  -- ^ The created color popup.
colorPopup field = colorPopup_ field def

-- | Creates a colorPopup using the given lens. Accepts config.
colorPopup_
  :: (WidgetModel s, WidgetEvent e)
  => ALens' s Color       -- ^ The lens into the model.
  -> [ColorPopupCfg s e]  -- ^ The config options.
  -> WidgetNode s e       -- ^ The created color popup.
colorPopup_ field configs = colorPopupD_ (WidgetLens field) configs

-- | Creates a colorPopup using the given value and 'onChange' event handler.
colorPopupV
  :: (WidgetModel s, WidgetEvent e)
  => Color           -- ^ The current value.
  -> (Color -> e)    -- ^ The event to raise on change.
  -> WidgetNode s e  -- ^ The created color popup.
colorPopupV value handler = colorPopupV_ value handler def

-- | Creates a colorPopup using the given value and 'onChange' event handler.
--   Accepts config.
colorPopupV_
  :: (WidgetModel s, WidgetEvent e)
  => Color                -- ^ The current value.
  -> (Color -> e)         -- ^ The event to raise on change.
  -> [ColorPopupCfg s e]  -- ^ The config options.
  -> WidgetNode s e       -- ^ The created color popup.
colorPopupV_ value handler configs = newNode where
  newConfigs = onChange handler : configs
  newNode = colorPopupD_ (WidgetValue value) newConfigs

-- | Creates a colorPopup providing a 'WidgetData' instance and config.
colorPopupD_
  :: (WidgetModel s, WidgetEvent e)
  => WidgetData s Color   -- ^ The 'WidgetData' to retrieve the value from.
  -> [ColorPopupCfg s e]  -- ^ The config options.
  -> WidgetNode s e       -- ^ The created color popup.
colorPopupD_ wdata configs = newNode where
  config = mconcat configs
  model = WidgetValue def
  uiBuilder = buildUI config
  eventHandler = handleEvent wdata config
  mergeModel wenv parentModel oldModel newModel = oldModel
    & popupColor .~ widgetDataGet parentModel wdata
  compCfg = [compositeMergeModel mergeModel]
  newNode = compositeD_ "colorPopup" model uiBuilder eventHandler compCfg

buildUI
  :: WidgetModel sp
  => ColorPopupCfg sp ep
  -> ColorPopupEnv
  -> ColorPopupModel
  -> ColorPopupNode
buildUI config wenv model = widgetTree where
  containerStyle = collectTheme wenv L.colorPopupStyle
  selColor = model ^. popupColor

  toggleStyle = mergeBasicStyle $ def
    & L.basic . non def . L.sizeReqW ?~ width 30
    & L.basic . non def . L.sizeReqH ?~ height 30
    & L.basic . non def . L.bgColor ?~ selColor
    & L.basic . non def . L.border ?~ border 1 selColor

  toggleCfg = [toggleButtonOffStyle toggleStyle]
  toggle = toggleButton_ "" popupShowColor toggleCfg
    & L.info . L.style .~ toggleStyle

  pickerCfg = _cpcColorPickerCfg config
  picker = colorPicker_ popupColor [pickerCfg, onChange ColorChanged]
    & L.info . L.style .~ containerStyle

  content = boxShadow picker
  popupCfg = [popupAlignToOuterV, popupOffset (Point 0 10), alignBottom, alignLeft]
  widgetTree = popup_ popupShowColor (popupAnchor toggle : popupCfg) content

handleEvent
  :: WidgetModel sp
  => WidgetData sp Color
  -> ColorPopupCfg sp ep
  -> ColorPopupEnv
  -> ColorPopupNode
  -> ColorPopupModel
  -> ColorPopupEvt
  -> [EventResponse ColorPopupModel ColorPopupEvt sp ep]
handleEvent wdata cfg wenv node model evt = case evt of
  PopupFocus prev
    | not (isNodeParentOfPath node prev) -> reportFocus prev
  PopupBlur next
    | not (isNodeParentOfPath node next) -> reportBlur next
  ColorChanged col -> reportChange col
  _ -> []
  where
    parentColor pm = widgetDataGet pm wdata
    parentChanged pm = parentColor pm /= model ^. popupColor

    report reqs = RequestParent <$> reqs
    reportFocus prev = report (($ prev) <$> _cpcOnFocusReq cfg)
    reportBlur next = report (($ next) <$> _cpcOnBlurReq cfg)
    reportChange col = report (wdataReqs ++ changeReqs) where
      wdataReqs = widgetDataSet wdata col
      changeReqs =  ($ col) <$> _cpcOnChangeReq cfg