packages feed

monomer-1.0.0.0: src/Monomer/Widgets/Containers/Box.hs

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

Container for a single item. Useful in different layout situations, since it
provides alignment options. This allows for the inner widget to keep its size
while being positioned more explicitly, while the box takes up the complete
space assigned by the container (in particular for containers which do not
follow SizeReq restriccions, such as Grid).
Can be used to add padding to an inner widget with a border. This is equivalent
to the margin property in CSS.
Also useful to handle click events in complex widget structures (for example, a
label with an image at its side).

Config:

- mergeRequired: function called during merge that receives the old and new
  model, returning True in case the child widget needs to be merged. Since by
  default merge is required, this function can be used to restrict merging when
  it would be expensive and it is not necessary. For example, a list of widgets
  representing search result only needs to be updated when the list of results
  changes, not while the user inputs new search criteria (which also triggers
  a model change and, hence, the merge process).
- ignoreEmptyArea: when the inner widget does not use all the available space,
  ignoring the unassigned space allows for mouse events to pass through. This is
  useful in zstack layers.
- sizeReqUpdater: allows modifying the 'SizeReq' generated by the inner widget.
- alignLeft: aligns the inner widget to the left.
- alignCenter: aligns the inner widget to the horizontal center.
- alignRight: aligns the inner widget to the right.
- alignTop: aligns the inner widget to the top.
- alignMiddle: aligns the inner widget to the left.
- alignBottom: aligns the inner widget to the bottom.
- onClick: click event.
- onClickReq: generates a WidgetRequest on click.
- onClickEmpty: click event on empty area.
- onClickEmptyReq: generates a WidgetRequest on click in empty area.
- expandContent: if the inner widget should use all the available space. To be
  able to use alignment options, this must be False (the default).
-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}

module Monomer.Widgets.Containers.Box (
  BoxCfg(..),
  box,
  box_,
  expandContent
) where

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

import qualified Data.Sequence as Seq

import Monomer.Widgets.Container
import Monomer.Widgets.Containers.Stack

import qualified Monomer.Lens as L

-- | Configuration options for box widget.
data BoxCfg s e = BoxCfg {
  _boxExpandContent :: Maybe Bool,
  _boxIgnoreEmptyArea :: Maybe Bool,
  _boxSizeReqUpdater :: Maybe SizeReqUpdater,
  _boxMergeRequired :: Maybe (s -> s -> Bool),
  _boxAlignH :: Maybe AlignH,
  _boxAlignV :: Maybe AlignV,
  _boxOnFocusReq :: [Path -> WidgetRequest s e],
  _boxOnBlurReq :: [Path -> WidgetRequest s e],
  _boxOnEnterReq :: [WidgetRequest s e],
  _boxOnLeaveReq :: [WidgetRequest s e],
  _boxOnClickReq :: [WidgetRequest s e],
  _boxOnClickEmptyReq :: [WidgetRequest s e],
  _boxOnBtnPressedReq :: [Button -> Int -> WidgetRequest s e],
  _boxOnBtnReleasedReq :: [Button -> Int -> WidgetRequest s e]
}

instance Default (BoxCfg s e) where
  def = BoxCfg {
    _boxExpandContent = Nothing,
    _boxIgnoreEmptyArea = Nothing,
    _boxSizeReqUpdater = Nothing,
    _boxMergeRequired = Nothing,
    _boxAlignH = Nothing,
    _boxAlignV = Nothing,
    _boxOnFocusReq = [],
    _boxOnBlurReq = [],
    _boxOnEnterReq = [],
    _boxOnLeaveReq = [],
    _boxOnClickReq = [],
    _boxOnClickEmptyReq = [],
    _boxOnBtnPressedReq = [],
    _boxOnBtnReleasedReq = []
  }

instance Semigroup (BoxCfg s e) where
  (<>) t1 t2 = BoxCfg {
    _boxExpandContent = _boxExpandContent t2 <|> _boxExpandContent t1,
    _boxIgnoreEmptyArea = _boxIgnoreEmptyArea t2 <|> _boxIgnoreEmptyArea t1,
    _boxSizeReqUpdater = _boxSizeReqUpdater t2 <|> _boxSizeReqUpdater t1,
    _boxMergeRequired = _boxMergeRequired t2 <|> _boxMergeRequired t1,
    _boxAlignH = _boxAlignH t2 <|> _boxAlignH t1,
    _boxAlignV = _boxAlignV t2 <|> _boxAlignV t1,
    _boxOnFocusReq = _boxOnFocusReq t1 <> _boxOnFocusReq t2,
    _boxOnBlurReq = _boxOnBlurReq t1 <> _boxOnBlurReq t2,
    _boxOnEnterReq = _boxOnEnterReq t1 <> _boxOnEnterReq t2,
    _boxOnLeaveReq = _boxOnLeaveReq t1 <> _boxOnLeaveReq t2,
    _boxOnClickReq = _boxOnClickReq t1 <> _boxOnClickReq t2,
    _boxOnClickEmptyReq = _boxOnClickEmptyReq t1 <> _boxOnClickEmptyReq t2,
    _boxOnBtnPressedReq = _boxOnBtnPressedReq t1 <> _boxOnBtnPressedReq t2,
    _boxOnBtnReleasedReq = _boxOnBtnReleasedReq t1 <> _boxOnBtnReleasedReq t2
  }

instance Monoid (BoxCfg s e) where
  mempty = def

instance CmbIgnoreEmptyArea (BoxCfg s e) where
  ignoreEmptyArea_ ignore = def {
    _boxIgnoreEmptyArea = Just ignore
  }

instance CmbSizeReqUpdater (BoxCfg s e) where
  sizeReqUpdater updater = def {
    _boxSizeReqUpdater = Just updater
  }

instance CmbMergeRequired (BoxCfg s e) s where
  mergeRequired fn = def {
    _boxMergeRequired = Just fn
  }

instance CmbAlignLeft (BoxCfg s e) where
  alignLeft_ False = def
  alignLeft_ True = def {
    _boxAlignH = Just ALeft
  }

instance CmbAlignCenter (BoxCfg s e) where
  alignCenter_ False = def
  alignCenter_ True = def {
    _boxAlignH = Just ACenter
  }

instance CmbAlignRight (BoxCfg s e) where
  alignRight_ False = def
  alignRight_ True = def {
    _boxAlignH = Just ARight
  }

instance CmbAlignTop (BoxCfg s e) where
  alignTop_ False = def
  alignTop_ True = def {
    _boxAlignV = Just ATop
  }

instance CmbAlignMiddle (BoxCfg s e) where
  alignMiddle_ False = def
  alignMiddle_ True = def {
    _boxAlignV = Just AMiddle
  }

instance CmbAlignBottom (BoxCfg s e) where
  alignBottom_ False = def
  alignBottom_ True = def {
    _boxAlignV = Just ABottom
  }

instance WidgetEvent e => CmbOnFocus (BoxCfg s e) e Path where
  onFocus handler = def {
    _boxOnFocusReq = [RaiseEvent . handler]
  }

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

instance WidgetEvent e => CmbOnBlur (BoxCfg s e) e Path where
  onBlur handler = def {
    _boxOnBlurReq = [RaiseEvent . handler]
  }

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

instance WidgetEvent e => CmbOnBtnPressed (BoxCfg s e) e where
  onBtnPressed handler = def {
    _boxOnBtnPressedReq = [(RaiseEvent .) . handler]
  }

instance CmbOnBtnPressedReq (BoxCfg s e) s e where
  onBtnPressedReq req = def {
    _boxOnBtnPressedReq = [req]
  }

instance WidgetEvent e => CmbOnBtnReleased (BoxCfg s e) e where
  onBtnReleased handler = def {
    _boxOnBtnReleasedReq = [(RaiseEvent .) . handler]
  }

instance CmbOnBtnReleasedReq (BoxCfg s e) s e where
  onBtnReleasedReq req = def {
    _boxOnBtnReleasedReq = [req]
  }

instance WidgetEvent e => CmbOnClick (BoxCfg s e) e where
  onClick handler = def {
    _boxOnClickReq = [RaiseEvent handler]
  }

instance CmbOnClickReq (BoxCfg s e) s e where
  onClickReq req = def {
    _boxOnClickReq = [req]
  }

instance WidgetEvent e => CmbOnClickEmpty (BoxCfg s e) e where
  onClickEmpty handler = def {
    _boxOnClickEmptyReq = [RaiseEvent handler]
  }

instance CmbOnClickEmptyReq (BoxCfg s e) s e where
  onClickEmptyReq req = def {
    _boxOnClickEmptyReq = [req]
  }

instance WidgetEvent e => CmbOnEnter (BoxCfg s e) e where
  onEnter handler = def {
    _boxOnEnterReq = [RaiseEvent handler]
  }

instance CmbOnEnterReq (BoxCfg s e) s e where
  onEnterReq req = def {
    _boxOnEnterReq = [req]
  }

instance WidgetEvent e => CmbOnLeave (BoxCfg s e) e where
  onLeave handler = def {
    _boxOnLeaveReq = [RaiseEvent handler]
  }

instance CmbOnLeaveReq (BoxCfg s e) s e where
  onLeaveReq req = def {
    _boxOnLeaveReq = [req]
  }

-- | Assigns all the available space to its contained child.
expandContent :: BoxCfg s e
expandContent = def {
  _boxExpandContent = Just True
}

newtype BoxState s = BoxState {
  _bxsModel :: Maybe s
}

-- | Creates a box widget with a single node as child.
box :: (WidgetModel s, WidgetEvent e) => WidgetNode s e -> WidgetNode s e
box managed = box_ def managed

-- | Creates a box widget with a single node as child. Accepts config.
box_
  :: (WidgetModel s, WidgetEvent e)
  => [BoxCfg s e]
  -> WidgetNode s e
  -> WidgetNode s e
box_ configs managed = makeNode (makeBox config state) managed where
  config = mconcat configs
  state = BoxState Nothing

makeNode :: Widget s e -> WidgetNode s e -> WidgetNode s e
makeNode widget managedWidget = defaultWidgetNode "box" widget
  & L.info . L.focusable .~ False
  & L.children .~ Seq.singleton managedWidget

makeBox
  :: (WidgetModel s, WidgetEvent e)
  => BoxCfg s e
  -> BoxState s
  -> Widget s e
makeBox config state = widget where
  widget = createContainer state def {
    containerIgnoreEmptyArea = ignoreEmptyArea && emptyHandlersCount == 0,
    containerGetCurrentStyle = getCurrentStyle,
    containerInit = init,
    containerMergeChildrenReq = mergeRequired,
    containerMerge = merge,
    containerHandleEvent = handleEvent,
    containerGetSizeReq = getSizeReq,
    containerResize = resize
  }

  ignoreEmptyArea = Just True == _boxIgnoreEmptyArea config
  emptyHandlersCount = length (_boxOnClickEmptyReq config)

  init wenv node = resultNode newNode where
    newState = BoxState (Just $ wenv ^. L.model)
    newNode = node
      & L.widget .~ makeBox config newState

  mergeRequired wenv node oldNode oldState = required where
    newModel = wenv ^. L.model
    required = case (_boxMergeRequired config, _bxsModel oldState) of
      (Just mergeReqFn, Just oldModel) -> mergeReqFn oldModel newModel
      _ -> True

  merge wenv node oldNode oldState = resultNode newNode where
    newState = BoxState (Just $ wenv ^. L.model)
    newNode = node
      & L.widget .~ makeBox config newState

  getCurrentStyle = currentStyle_ currentStyleConfig where
    currentStyleConfig = def
      & L.isActive .~ isNodeTreeActive

  handleEvent wenv node target evt = case evt of
    Focus prev -> handleFocusChange node prev (_boxOnFocusReq config)
    Blur next -> handleFocusChange node next (_boxOnBlurReq config)

    Enter point
      | not (null reqs) && inChildVp point -> result where
        reqs = _boxOnEnterReq config
        result = Just (resultReqs node reqs)

    Leave point
      | not (null reqs) -> result where
        reqs = _boxOnLeaveReq config
        result = Just (resultReqs node reqs)

    Click point btn _
      | not (null reqs) && inChildVp point -> result where
        reqs = _boxOnClickReq config
        result = Just (resultReqs node reqs)

    Click point btn _
      | not (null reqs) && not (inChildVp point) -> result where
        reqs = _boxOnClickEmptyReq config
        result = Just (resultReqs node reqs)

    ButtonAction point btn BtnPressed clicks
      | not (null reqs) && inChildVp point -> result where
        reqs = _boxOnBtnPressedReq config <*> pure btn <*> pure clicks
        result = Just (resultReqs node reqs)

    ButtonAction point btn BtnReleased clicks
      | clicks == 1 && not (null reqs) && inChildVp point -> result where
        reqs = _boxOnBtnReleasedReq config <*> pure btn <*> pure clicks
        result = Just (resultReqs node reqs)

    ButtonAction point btn BtnReleased clicks
      | clicks > 1 && not (null reqs) && inChildVp point -> result where
        reqsA = _boxOnClickReq config
        reqsB = _boxOnBtnReleasedReq config <*> pure btn <*> pure clicks
        reqs = reqsA <> reqsB
        result = Just (resultReqs node reqs)

    ButtonAction point btn BtnReleased clicks
      | clicks > 1 && not (null reqs) && not (inChildVp point) -> result where
        reqs = _boxOnClickEmptyReq config
        result = Just (resultReqs node reqs)

    _ -> Nothing
    where
      child = Seq.index (node ^. L.children) 0
      inChildVp point  = pointInRect point (child ^. L.info . L.viewport)

  getSizeReq :: ContainerGetSizeReqHandler s e
  getSizeReq wenv node children = newSizeReq where
    updateSizeReq = fromMaybe id (_boxSizeReqUpdater config)
    child = Seq.index children 0
    newReqW = child ^. L.info . L.sizeReqW
    newReqH = child ^. L.info . L.sizeReqH
    newSizeReq = updateSizeReq (newReqW, newReqH)

  resize wenv node viewport children = resized where
    style = getCurrentStyle wenv node
    child = Seq.index children 0
    contentArea = fromMaybe def (removeOuterBounds style viewport)
    Rect cx cy cw ch = contentArea

    contentW = snd $ assignStackAreas True contentArea children
    contentH = snd $ assignStackAreas False contentArea children

    raChild = Rect cx cy (min cw contentW) (min ch contentH)
    ah = fromMaybe ACenter (_boxAlignH config)
    av = fromMaybe AMiddle (_boxAlignV config)
    raAligned = alignInRect contentArea raChild ah av

    expand = fromMaybe False (_boxExpandContent config)
    resized
      | expand = (resultNode node, Seq.singleton contentArea)
      | otherwise = (resultNode node, Seq.singleton raAligned)