packages feed

monomer-1.5.0.0: src/Monomer/Widgets/Containers/BoxShadow.hs

{-|
Module      : Monomer.Widgets.Containers.BoxShadow
Copyright   : (c) 2022 Gareth Smith, Francisco Vallarino
License     : BSD-3-Clause (see the LICENSE file)
Maintainer  : fjvallarino@gmail.com
Stability   : experimental
Portability : non-portable

A rectangular drop-shadow. Normally used around alert boxes to give the illusion
they are floating above the widgets underneath them.
-}
{-# LANGUAGE Strict #-}

module Monomer.Widgets.Containers.BoxShadow (
  -- * Configuration
  BoxShadowCfg,
  -- * Constructors
  boxShadow,
  boxShadow_
) where

import Control.Applicative ((<|>))
import Control.Lens ((&), (.~), (^.))
import Data.Default
import Data.Maybe

import qualified Data.Sequence as Seq

import Monomer.Core
import Monomer.Core.Combinators

import Monomer.Widgets.Container

import qualified Monomer.Lens as L

{-|
Configuration options for boxShadow:

- 'radius': the radius of the corners of the shadow.
- 'alignLeft': aligns the shadow to the left.
- 'alignCenter': aligns the shadow to the horizontal center.
- 'alignRight': aligns the shadow to the right.
- 'alignTop': aligns the shadow to the top.
- 'alignMiddle': aligns the shadow to the vertical middle.
- 'alignBottom': aligns the shadow to the bottom.
-}
data BoxShadowCfg = BoxShadowCfg {
  _bscRadius :: Maybe Double,
  _bscAlignH :: Maybe AlignH,
  _bscAlignV :: Maybe AlignV
}

instance Default BoxShadowCfg where
  def = BoxShadowCfg {
    _bscRadius = Nothing,
    _bscAlignH = Nothing,
    _bscAlignV = Nothing
  }

instance Semigroup BoxShadowCfg where
  (<>) c1 c2 = BoxShadowCfg {
    _bscRadius = _bscRadius c1 <|> _bscRadius c2,
    _bscAlignH = _bscAlignH c1 <|> _bscAlignH c2,
    _bscAlignV = _bscAlignV c1 <|> _bscAlignV c2
  }

instance Monoid BoxShadowCfg where
  mempty = def

instance CmbRadius BoxShadowCfg where
  radius r = def {
    _bscRadius = Just r
  }

instance CmbAlignLeft BoxShadowCfg where
  alignLeft_ False = def
  alignLeft_ True = def {
    _bscAlignH = Just ALeft
  }

instance CmbAlignCenter BoxShadowCfg where
  alignCenter_ False = def
  alignCenter_ True = def {
    _bscAlignH = Just ACenter
  }

instance CmbAlignRight BoxShadowCfg where
  alignRight_ False = def
  alignRight_ True = def {
    _bscAlignH = Just ARight
  }

instance CmbAlignTop BoxShadowCfg where
  alignTop_ False = def
  alignTop_ True = def {
    _bscAlignV = Just ATop
  }

instance CmbAlignMiddle BoxShadowCfg where
  alignMiddle_ False = def
  alignMiddle_ True = def {
    _bscAlignV = Just AMiddle
  }

instance CmbAlignBottom BoxShadowCfg where
  alignBottom_ False = def
  alignBottom_ True = def {
    _bscAlignV = Just ABottom
  }

-- | Creates a boxShadow around the provided content.
boxShadow
  :: WidgetNode s e  -- ^ The content to display inside the boxShadow.
  -> WidgetNode s e  -- ^ The created boxShadow.
boxShadow = boxShadow_ def

-- | Creates a boxShadow around the provided content. Accepts config.
boxShadow_
  :: [BoxShadowCfg]  -- ^ The config options for the boxShadow.
  -> WidgetNode s e  -- ^ The content to display inside the boxShadow.
  -> WidgetNode s e  -- ^ The created boxShadow.
boxShadow_ config child =
  defaultWidgetNode "boxShadow" (boxShadowWidget (mconcat config))
   & L.children .~ Seq.singleton child

boxShadowWidget :: BoxShadowCfg -> Widget s e
boxShadowWidget config = widget where
  widget = createContainer () def {
    containerGetSizeReq = getSizeReq,
    containerResize = resize,
    containerRender = render
  }

  shadowRadius = fromMaybe 8 (_bscRadius config)
  shadowDiameter = shadowRadius * 2

  getSizeReq wenv node children = (sizeReqW, sizeReqH) where
    sizeReqW = maybe (fixedSize 0) (addFixed shadowDiameter . _wniSizeReqW . _wnInfo) vchild
    sizeReqH = maybe (fixedSize 0) (addFixed shadowDiameter. _wniSizeReqH . _wnInfo) vchild
    vchildren = Seq.filter (_wniVisible . _wnInfo) children
    vchild = Seq.lookup 0 vchildren

  resize wenv node viewport children = (resultNode node, fmap assignArea children) where
    style = currentStyle wenv node
    contentArea = fromMaybe def (removeOuterBounds style viewport)
    
    assignArea child
      | visible = moveRect childOffset (subtractShadow contentArea)
      | otherwise = def
      where
        visible = (_wniVisible . _wnInfo) child
    
    childOffset = Point offsetX offsetY where
      theme = currentTheme wenv node
      shadowAlignH = fromMaybe (theme ^. L.shadowAlignH) (_bscAlignH config)
      shadowAlignV = fromMaybe (theme ^. L.shadowAlignV) (_bscAlignV config)
      offset = shadowRadius / 4
      offsetX = case shadowAlignH of
        ALeft -> offset
        ACenter -> 0
        ARight -> -offset
      offsetY = case shadowAlignV of
        ATop -> offset
        AMiddle -> 0
        ABottom -> -offset
  
  render wenv node renderer = do
    beginPath renderer
    setFillBoxGradient renderer (subtractShadow vp) shadowRadius shadowDiameter shadowColor transparent
    renderRect renderer vp
    fill renderer
    where
      style = currentStyle wenv node
      vp = getContentArea node style
      shadowColor = wenv ^. L.theme . L.basic . L.shadowColor
      transparent = rgba 0 0 0 0
  
  subtractShadow (Rect l t w h) = Rect l' t' w' h' where
    (l', w') = subtractDim l w
    (t', h') = subtractDim t h
    subtractDim pos size
      | size > shadowDiameter = (pos + shadowRadius, size - shadowDiameter)
      | otherwise = (pos + size / 2, 0)

addFixed :: Double -> SizeReq -> SizeReq
addFixed f sReq =
  sReq { _szrFixed = _szrFixed sReq + f }