packages feed

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

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

Layered container, stacking children one on top of the other. Useful for
handling widgets that need to be visible in certain contexts only (dialogs), or
to overlay unrelated widgets (text on top of an image).

The order of the widgets is from bottom to top.

The container will request the largest horizontal and vertical size from its
child nodes.

Config:

- onlyTopActive: whether the top visible node is the only node that may receive
events.
-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}

module Monomer.Widgets.Containers.ZStack (
  zstack,
  zstack_,
  onlyTopActive
) where

import Control.Applicative ((<|>))
import Control.Lens ((&), (^.), (^?), (.~), (%~), (?~), at, ix)
import Control.Monad (forM_, void, when)
import Data.Default
import Data.Maybe
import Data.List (foldl', any)
import Data.Sequence (Seq(..), (<|), (|>))
import GHC.Generics

import qualified Data.Map.Strict as M
import qualified Data.Sequence as Seq

import Monomer.Widgets.Container

import qualified Monomer.Lens as L

newtype ZStackCfg = ZStackCfg {
  _zscOnlyTopActive :: Maybe Bool
}

instance Default ZStackCfg where
  def = ZStackCfg Nothing

instance Semigroup ZStackCfg where
  (<>) z1 z2 = ZStackCfg {
    _zscOnlyTopActive = _zscOnlyTopActive z2 <|> _zscOnlyTopActive z1
  }

instance Monoid ZStackCfg where
  mempty = def

-- | Whether the top visible node is the only node that may receive events.
-- | Defaults to True.
onlyTopActive :: Bool -> ZStackCfg
onlyTopActive active = def {
  _zscOnlyTopActive = Just active
}

data ZStackState = ZStackState {
  _zssFocusMap :: M.Map PathStep WidgetId,
  _zssTopIdx :: Int
} deriving (Eq, Show, Generic)

-- | Creates a zstack container with the provided nodes.
zstack :: (Traversable t) => t (WidgetNode s e) -> WidgetNode s e
zstack children = zstack_ def children

-- | Creates a zstack container with the provided nodes. Accepts config.
zstack_
  :: (Traversable t)
  => [ZStackCfg]
  -> t (WidgetNode s e)
  -> WidgetNode s e
zstack_ configs children = newNode where
  config = mconcat configs
  state = ZStackState M.empty 0
  newNode = defaultWidgetNode "zstack" (makeZStack config state)
    & L.children .~ Seq.reverse (foldl' (|>) Empty children)

makeZStack :: ZStackCfg -> ZStackState -> Widget s e
makeZStack config state = widget where
  baseWidget = createContainer state def {
    containerUseChildrenSizes = True,
    containerInit = init,
    containerMergePost = mergePost,
    containerFindNextFocus = findNextFocus,
    containerGetSizeReq = getSizeReq,
    containerResize = resize
  }
  widget = baseWidget {
    widgetFindByPoint = findByPoint,
    widgetRender = render
  }

  onlyTopActive = fromMaybe True (_zscOnlyTopActive config)

  init wenv node = resultNode newNode where
    children = node ^. L.children
    focusedPath = wenv ^. L.focusedPath
    newState = state {
      _zssTopIdx = fromMaybe 0 (Seq.findIndexL (^.L.info . L.visible) children)
    }
    newNode = node
      & L.widget .~ makeZStack config newState

  mergePost wenv node oldNode oldState newState result = newResult where
    ZStackState oldFocusMap oldTopIdx = oldState
    children = node ^. L.children
    focusedPath = wenv ^. L.focusedPath
    focusedWid = findWidgetIdFromPath wenv focusedPath
    isFocusParent = isNodeParentOfPath node focusedPath

    topLevel = isNodeTopLevel wenv node
    flagsChanged = childrenFlagsChanged oldNode node
    newTopIdx = fromMaybe 0 (Seq.findIndexL (^.L.info . L.visible) children)
    focusReq = isJust $ Seq.findIndexL isFocusRequest (result ^. L.requests)
    needsFocus = isFocusParent && topLevel && flagsChanged && not focusReq

    oldTopWid = M.lookup newTopIdx oldFocusMap
    fstTopWid = node ^? L.children . ix newTopIdx . L.info . L.widgetId
    newState = oldState {
      _zssFocusMap = oldFocusMap & at oldTopIdx .~ focusedWid,
      _zssTopIdx = newTopIdx
    }

    tmpResult = result
      & L.node . L.widget .~ makeZStack config newState
    newResult
      | needsFocus && isJust oldTopWid = tmpResult
          & L.requests %~ (|> SetFocus (fromJust oldTopWid))
      | needsFocus = tmpResult
          & L.requests %~ (|> MoveFocus fstTopWid FocusFwd)
      | isFocusParent = tmpResult
      | otherwise = result

  -- | Find instance matching point
  findByPoint wenv node start point = result where
    children = node ^. L.children
    vchildren
      | onlyTopActive = Seq.take 1 $ Seq.filter (_wniVisible . _wnInfo) children
      | otherwise = Seq.filter (_wniVisible . _wnInfo) children

    nextStep = nextTargetStep node start
    ch = Seq.index children (fromJust nextStep)
    visible = node ^. L.info . L.visible
    childVisible = ch ^. L.info . L.visible
    isNextValid = isJust nextStep && visible && childVisible
    result
      | isNextValid = widgetFindByPoint (ch ^. L.widget) wenv ch start point
      | visible = findFirstByPoint vchildren wenv start point
      | otherwise = Nothing

  findNextFocus wenv node direction start = result where
    children = node ^. L.children
    vchildren = Seq.filter (_wniVisible . _wnInfo) children
    result
      | onlyTopActive = Seq.take 1 vchildren
      | otherwise = vchildren

  getSizeReq wenv node children = (newSizeReqW, newSizeReqH) where
    vchildren = Seq.filter (_wniVisible . _wnInfo) children
    newSizeReqW = getDimSizeReq (_wniSizeReqW . _wnInfo) vchildren
    newSizeReqH = getDimSizeReq (_wniSizeReqH . _wnInfo) vchildren

  getDimSizeReq accesor vchildren
    | Seq.null vreqs = fixedSize 0
    | otherwise = foldl1 sizeReqMergeMax vreqs
    where
      vreqs = accesor <$> vchildren

  resize wenv node viewport children = resized where
    style = currentStyle wenv node
    vpChild = fromMaybe def (removeOuterBounds style viewport)
    assignedAreas = fmap (const vpChild) children
    resized = (resultNode node, assignedAreas)

  render wenv node renderer =
    drawInScissor renderer True viewport $
      drawStyledAction renderer viewport style $ \_ ->
        void $ Seq.traverseWithIndex renderChild children
    where
      style = currentStyle wenv node
      children = Seq.reverse $ node ^. L.children
      viewport = node ^. L.info . L.viewport
      isVisible c = c ^. L.info . L.visible
      topVisibleIdx = fromMaybe 0 (Seq.findIndexR (_wniVisible . _wnInfo) children)

      isPointEmpty point idx = not covered where
        prevs = Seq.drop (idx + 1) children
        target c = widgetFindByPoint (c ^. L.widget) wenv c emptyPath point
        isCovered c = isVisible c && isJust (target c)
        covered = any isCovered prevs

      isTopLayer idx child point = prevTopLayer && isValid where
        prevTopLayer = _weInTopLayer wenv point
        isValid
          | onlyTopActive = idx == topVisibleIdx
          | otherwise = isPointEmpty point idx

      cWenv idx child = wenv {
        _weInTopLayer = isTopLayer idx child
      }
      renderChild idx child = when (isVisible child) $
        widgetRender (child ^. L.widget) (cWenv idx child) child renderer

findFirstByPoint
  :: Seq (WidgetNode s e)
  -> WidgetEnv s e
  -> Seq PathStep
  -> Point
  -> Maybe WidgetNodeInfo
findFirstByPoint Empty _ _ _ = Nothing
findFirstByPoint (ch :<| chs) wenv start point = result where
  isVisible = ch ^. L.info . L.visible
  newPath = widgetFindByPoint (ch ^. L.widget) wenv ch start point
  result
    | isVisible && isJust newPath = newPath
    | otherwise = findFirstByPoint chs wenv start point